From bae9752c5aec679a6737161eba0dbd15f334b1b8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 15 Jul 2026 22:40:00 -0700 Subject: [PATCH] refactor(deadcode): enforce repository hard zero (#108641) --- .../scripts/update-main.mjs | 8 +- .../scripts/secret-scanning.mjs | 6 +- config/knip.all-exports.config.ts | 132 ++++++++ config/knip.config.ts | 310 ++++++++++++++---- config/knip.scripts-exports.config.ts | 76 +++++ .../chrome-extension/modules/relay-core.js | 2 +- extensions/browser/test-support.ts | 11 +- .../app-server/run-attempt-test-harness.ts | 2 +- .../session-binding.test-helpers.ts | 4 - .../src/app-server/shared-client.test.ts | 15 +- .../codex/src/app-server/shared-client.ts | 6 +- extensions/copilot/package.json | 1 - extensions/matrix/src/test-helpers.ts | 43 --- extensions/msteams/package.json | 2 + extensions/nostr/src/test-fixtures.ts | 2 - extensions/nostr/test/setup.ts | 5 - .../src/doctor/register.test-harness.ts | 4 +- extensions/qa-lab/package.json | 3 +- extensions/qa-lab/src/ci-smoke-plan.ts | 2 +- .../src/ui-render-capture-timeline-model.ts | 2 - extensions/qa-lab/web/src/ui-types.ts | 18 +- .../commands/slash-command-test-support.ts | 2 +- extensions/slack/src/monitor/media.ts | 4 +- extensions/slack/src/monitor/thread.ts | 2 +- .../src/bot-message-dispatch.test-harness.ts | 46 +-- .../bot.create-telegram-bot.test-harness.ts | 6 +- .../telegram/src/bot.media.e2e-harness.ts | 2 +- extensions/twitch/test/setup.ts | 7 - .../voice-call/src/manager.test-harness.ts | 2 +- extensions/whatsapp/constants.ts | 2 - .../whatsapp/src/auto-reply.test-harness.ts | 1 - .../src/monitor-inbox.test-harness.ts | 4 +- extensions/whatsapp/targets.ts | 6 - package.json | 10 +- packages/ai/src/providers/google-shared.ts | 10 +- .../gateway-protocol/src/schema/artifacts.ts | 4 +- packages/memory-host-sdk/package.json | 3 + pnpm-lock.yaml | 49 ++- scripts/check-deadcode-exports.mjs | 64 +++- scripts/check-deadcode-unused-files.d.mts | 71 ++-- scripts/check-deadcode-unused-files.mjs | 142 ++++---- scripts/check-duplicates.mjs | 2 +- scripts/deadcode-knip-runner.mjs | 25 +- scripts/deadcode-unused-files.allowlist.mjs | 40 --- scripts/dev/gateway-smoke.ts | 5 - .../runtime-smoke.mjs | 2 +- scripts/e2e/lib/gateway-network/client.mjs | 2 +- scripts/e2e/lib/mcp-code-mode-validation.ts | 2 +- scripts/e2e/lib/plugin-index-sqlite.d.mts | 2 - scripts/e2e/lib/plugin-index-sqlite.mjs | 4 +- scripts/e2e/parallels/common.ts | 19 +- scripts/e2e/parallels/filesystem.ts | 8 +- scripts/e2e/secret-provider-integrations.mjs | 3 - scripts/e2e/telegram-user-crabbox-proof.ts | 2 +- .../generate-secretref-credential-matrix.ts | 15 - scripts/github/real-behavior-proof-policy.mjs | 2 +- scripts/lib/actions-artifact-archive.mjs | 6 +- .../lib/codex-app-server-protocol-source.ts | 2 +- scripts/lib/cross-os-release-checks/config.ts | 10 +- .../lib/cross-os-release-checks/install.ts | 14 +- scripts/lib/docker-e2e-plan.d.mts | 1 - scripts/lib/docker-e2e-plan.mjs | 1 - scripts/lib/docker-plugin-selection.mjs | 2 +- scripts/lib/gateway-bench-child.ts | 2 +- scripts/lib/gateway-ws-client.ts | 4 +- scripts/lib/openclaw-test-state.mjs | 2 +- scripts/lib/plain-gh.d.mts | 16 - scripts/lib/plugin-gateway-gauntlet.d.mts | 1 - scripts/lib/plugin-gateway-gauntlet.mjs | 1 - scripts/lib/plugin-npm-release.ts | 6 +- scripts/lib/sqlite-reliability-contract.ts | 4 +- scripts/lib/sqlite-reliability-writer.ts | 4 +- scripts/lib/sqlite-session-schema-baseline.ts | 4 +- .../build-telegram-desktop-proof-evidence.mjs | 2 +- scripts/mcp-code-mode-gateway-e2e.ts | 2 +- scripts/measure-rpc-rtt.mjs | 2 +- scripts/openclaw-cross-os-release-checks.ts | 2 +- scripts/openclaw-npm-resume-run.mjs | 2 +- scripts/perf/summarize-cpuprofile.mjs | 4 +- scripts/qa/ux-matrix-evidence-producer.ts | 2 +- scripts/render-github-release-notes.mjs | 6 +- scripts/report-test-temp-creations.mjs | 2 +- scripts/sqlite-vec-smoke.mjs | 41 --- scripts/sync-openclaw-label-colors.mjs | 302 ----------------- scripts/test-docker-all.mjs | 3 +- scripts/test-projects.test-support.mjs | 4 +- scripts/testbox-lease-freshness.mjs | 2 +- scripts/verify-pr-hosted-gates.mjs | 2 +- scripts/verify-template-cache-bound.mjs | 137 -------- src/acp/runtime/adapter-contract.testkit.ts | 122 ------- .../anthropic-transport-stream.live.test.ts | 2 +- src/agents/bundle-mcp-shared.test-harness.ts | 93 ------ ...dded-agent-runner-extraparams.live.test.ts | 2 +- .../compact.hooks.harness.ts | 13 +- .../run.overflow-compaction.harness.ts | 36 +- src/agents/live-cache-regression-policy.ts | 237 +++++++++++++ .../live-cache-regression-runner.test.ts | 65 ++-- src/agents/live-cache-regression-runner.ts | 259 +-------------- .../live-test-provider-drift.test-support.ts | 20 ++ src/agents/live-test-provider-drift.test.ts | 4 +- src/agents/live-test-provider-drift.ts | 8 +- src/agents/models.profiles.live.test.ts | 4 +- .../openai-transport-stream.test-harness.ts | 2 +- src/agents/test-helpers/fast-tool-stubs.ts | 2 +- .../tools/image-tool.providers.live.test.ts | 2 +- ...ispatch-from-config.shared.test-harness.ts | 2 - .../dispatch-from-config.test-harness.ts | 12 +- .../reply/session-entry-persistence.ts | 2 +- .../group-policy-contract-suites.ts | 2 +- src/cli/plugins-cli-test-helpers.ts | 2 +- src/commands/test-wizard-helpers.ts | 1 - src/config/sessions/test-helpers.ts | 23 -- .../transcript-append.test-support.ts | 6 +- src/cron/isolated-agent.turn-test-helpers.ts | 56 +--- src/cron/isolated-agent/run.test-harness.ts | 14 +- .../service.issue-regressions.test-helpers.ts | 17 +- .../gateway-models.profiles.live.test.ts | 4 +- src/gateway/server-http.test-harness.ts | 71 ---- .../server-methods/agent.test-harness.ts | 34 +- .../server-methods/chat.test-helpers.ts | 29 -- ...server.agent.gateway-server-agent.mocks.ts | 2 +- src/gateway/server.auth.test-helpers.ts | 6 - src/gateway/server.e2e-ws-harness.ts | 2 +- src/gateway/test-helpers.lan-pairing.ts | 4 +- src/gateway/test-helpers.mocks.ts | 33 +- src/gateway/test-helpers.runtime-state.ts | 1 - src/gateway/test-helpers.ts | 1 - .../test/server-sessions.test-helpers.ts | 22 -- .../placement-dispatch-test-fixtures.ts | 4 +- src/infra/outbound/targets.test-helpers.ts | 2 +- src/infra/sqlite-pragma.test-support.ts | 2 +- .../test-helpers/provider-auth-contract.ts | 2 +- src/plugins/hooks.test-fixtures.ts | 5 +- src/plugins/loader.test-harness.ts | 12 +- src/plugins/memory-state.test-fixtures.ts | 12 - src/process/exec-spawn.ts | 2 +- src/process/exec.ts | 1 - .../runtime.integration.test-helpers.ts | 5 - src/skills/loading/workspace.ts | 1 - src/skills/security/scanner.ts | 4 +- src/state/sqlite-schema-shape.test-support.ts | 2 +- src/system-agent/system-agent.test-helpers.ts | 2 +- .../bundled-plugin-public-surface.ts | 127 +------ src/test-utils/deferred.ts | 2 +- src/test-utils/mock-http.ts | 6 +- src/test-utils/openclaw-test-state.ts | 2 +- src/test-utils/process-tree.ts | 2 +- .../qa-lab/runtime/docker-e2e-lane.fixture.ts | 13 +- .../runtime/heartbeat-active-hours-runtime.ts | 2 - .../qa-lab/runtime/mcp-channels.fixture.ts | 9 +- .../runtime/mcp-client-temp-state.fixture.ts | 2 +- .../openai-image-auth-docker-client.ts | 2 +- test/e2e/qa-lab/runtime/script-evidence.ts | 2 +- test/fixtures/child-process-bridge/child.js | 11 - test/global-setup.ts | 7 - test/helpers/acp-manager-task-state.ts | 2 - .../agents/happy-path-prompt-snapshots.ts | 2 +- .../agents/prompt-composition-scenarios.ts | 2 +- test/helpers/auth-wizard.ts | 9 - test/helpers/config/config-honor-audit.ts | 2 +- .../cron/service-regression-fixtures.ts | 4 - test/helpers/gateway-e2e-harness.ts | 5 +- test/helpers/image-fixtures.ts | 2 - .../bundled-provider-builders.ts | 17 +- test/helpers/openclaw-test-instance.ts | 7 +- test/helpers/plugins/plugin-sdk-stub.cjs | 64 ---- .../sqlite-sessions-transcripts-flip-proof.ts | 2 +- test/helpers/temp-dir.ts | 8 +- test/helpers/ui-style-fixtures.ts | 2 +- test/image-generation.runtime.live.test.ts | 2 +- test/scripts/check-deadcode-exports.test.ts | 130 +++++++- .../check-deadcode-unused-files.test.ts | 99 +++--- test/scripts/plugin-gateway-gauntlet.test.ts | 17 - test/vitest/vitest.bundled-plugin-paths.ts | 1 - test/vitest/vitest.channel-paths.mjs | 8 +- .../vitest.extension-channel-split-paths.mjs | 2 +- test/vitest/vitest.extension-feishu-paths.mjs | 2 +- test/vitest/vitest.extension-matrix-paths.mjs | 2 +- .../vitest.extension-mattermost-paths.mjs | 2 +- .../vitest.extension-messaging-paths.mjs | 2 +- .../vitest/vitest.extension-msteams-paths.mjs | 2 +- .../vitest.extension-provider-paths.mjs | 4 +- .../vitest.extension-voice-call-paths.mjs | 2 +- .../vitest.extension-whatsapp-paths.mjs | 2 +- test/vitest/vitest.system-load.ts | 2 +- test/vitest/vitest.unit-fast-paths.mjs | 9 +- ui/package.json | 2 + 187 files changed, 1413 insertions(+), 2284 deletions(-) create mode 100644 config/knip.all-exports.config.ts create mode 100644 config/knip.scripts-exports.config.ts delete mode 100644 extensions/matrix/src/test-helpers.ts delete mode 100644 extensions/nostr/test/setup.ts delete mode 100644 extensions/twitch/test/setup.ts delete mode 100644 extensions/whatsapp/constants.ts delete mode 100644 extensions/whatsapp/targets.ts delete mode 100644 scripts/deadcode-unused-files.allowlist.mjs delete mode 100644 scripts/generate-secretref-credential-matrix.ts delete mode 100644 scripts/sqlite-vec-smoke.mjs delete mode 100644 scripts/sync-openclaw-label-colors.mjs delete mode 100644 scripts/verify-template-cache-bound.mjs delete mode 100644 src/acp/runtime/adapter-contract.testkit.ts create mode 100644 src/agents/live-cache-regression-policy.ts create mode 100644 src/agents/live-test-provider-drift.test-support.ts delete mode 100644 src/gateway/server-methods/chat.test-helpers.ts delete mode 100644 test/fixtures/child-process-bridge/child.js delete mode 100644 test/global-setup.ts delete mode 100644 test/helpers/plugins/plugin-sdk-stub.cjs diff --git a/.agents/skills/openclaw-live-updater/scripts/update-main.mjs b/.agents/skills/openclaw-live-updater/scripts/update-main.mjs index b4f3b765d157..2a5c210502ad 100644 --- a/.agents/skills/openclaw-live-updater/scripts/update-main.mjs +++ b/.agents/skills/openclaw-live-updater/scripts/update-main.mjs @@ -53,7 +53,7 @@ exec "$@" const DEPENDENCY_INPUT_RE = /^(?:\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|patches\/)|(?:^|\/)package\.json$/u; -export class UpdateInvariantError extends Error { +class UpdateInvariantError extends Error { constructor(code, message) { super(message); this.name = "UpdateInvariantError"; @@ -282,7 +282,7 @@ export function inspectBuildState(checkout, expectedSha) { }; } -export function verifyCheckout(checkout, { remote }) { +function verifyCheckout(checkout, { remote }) { let resolvedCheckout; try { resolvedCheckout = realpathSync(checkout); @@ -369,7 +369,7 @@ export function verifyCheckout(checkout, { remote }) { }; } -export function updateMain({ checkout, remote }, dependencies = {}) { +function updateMain({ checkout, remote }, dependencies = {}) { const before = verifyCheckout(checkout, { remote }); const fetchMain = dependencies.fetchMain ?? @@ -2164,7 +2164,7 @@ function parseArgs(argv) { return options; } -export function main(argv = process.argv.slice(2)) { +function main(argv = process.argv.slice(2)) { try { console.log(JSON.stringify(maintainMain(parseArgs(argv)))); } catch (error) { diff --git a/.agents/skills/openclaw-secret-scanning-maintainer/scripts/secret-scanning.mjs b/.agents/skills/openclaw-secret-scanning-maintainer/scripts/secret-scanning.mjs index f208f34fb787..fa1f6a51b39c 100644 --- a/.agents/skills/openclaw-secret-scanning-maintainer/scripts/secret-scanning.mjs +++ b/.agents/skills/openclaw-secret-scanning-maintainer/scripts/secret-scanning.mjs @@ -60,7 +60,7 @@ function isBodyLocationType(locationType) { } /** Decides whether redacting an issue/PR body requires notifying the reporter. */ -export function decideBodyRedaction(currentBody, redactedBody) { +function decideBodyRedaction(currentBody, redactedBody) { const bodyChanged = String(currentBody) !== String(redactedBody); return { body_changed: bodyChanged, @@ -69,7 +69,7 @@ export function decideBodyRedaction(currentBody, redactedBody) { } /** Loads redaction-result metadata for issue/PR body secret locations. */ -export function loadBodyRedactionResult(locationType, resultFile) { +function loadBodyRedactionResult(locationType, resultFile) { if (!isBodyLocationType(locationType)) { return { notify_required: true }; } @@ -887,7 +887,7 @@ function cmdSummary(jsonFile) { const args = []; -export const commands = { +const commands = { "fetch-alert": () => cmdFetchAlert(args[0]), "fetch-content": () => cmdFetchContent(args[0]), "redact-body": () => cmdRedactBody(args[0], args[1], args[2]), diff --git a/config/knip.all-exports.config.ts b/config/knip.all-exports.config.ts new file mode 100644 index 000000000000..915a67861896 --- /dev/null +++ b/config/knip.all-exports.config.ts @@ -0,0 +1,132 @@ +/** + * Full-repository unused-export audit. + * + * Production Knip intentionally ignores test-support exports. This companion + * config makes every test/spec file an entry and audits those support modules + * too, so a helper used by a src/ or plugin test cannot be mistaken for dead + * code while genuinely unused test and script exports still fail the gate. + */ +import fs from "node:fs"; +import path from "node:path"; +import YAML from "yaml"; +import productionConfig from "./knip.config.ts"; + +const TEST_ENTRY_GLOB = "**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!"; + +function listQaScenarioExecutionEntries(dir = "qa/scenarios"): string[] { + const entries = fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + return listQaScenarioExecutionEntries(entryPath); + } + if (!entry.isFile() || (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml"))) { + return []; + } + const document = YAML.parse(fs.readFileSync(entryPath, "utf8")) as { + scenario?: { execution?: { kind?: unknown; path?: unknown } }; + }; + const execution = document.scenario?.execution; + return execution?.kind !== "flow" && typeof execution?.path === "string" + ? [`${execution.path}!`] + : []; + }); + return [...new Set(entries)].toSorted((left, right) => left.localeCompare(right)); +} + +const QA_SCENARIO_EXECUTION_ENTRIES = listQaScenarioExecutionEntries(); +const ROOT_TEST_ENTRY_GLOBS = [ + "*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + // Vitest loads these by configuration or module alias rather than imports. + "test/setup*.ts!", + "test/non-isolated-runner.ts!", + "test/vitest/*-runtime.ts!", + "test/vitest/vitest*.config.ts!", + // Test drivers and Docker fixtures are executed by path from package scripts + // and the test-project registry. + "test/e2e/qa-lab/runtime/agent-bundle-mcp-tools-docker-client.ts!", + "test/e2e/qa-lab/runtime/docker-e2e-lane.ts!", + "test/e2e/qa-lab/runtime/mcp-channels-docker-client.ts!", + // Invoked directly by the Docker image-auth scenario. + "test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts!", + "test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts!", + // QA scenario YAML dispatches these scripts/tests by path rather than import. + ...QA_SCENARIO_EXECUTION_ENTRIES, + // The Voice Call QA scenario loads this fixture through a generated plugin directory. + "test/e2e/qa-lab/runtime/fixtures/voice-call-runtime-plugin/index.js!", + "test/scripts/fixtures/secret-provider-integrations-harness.mjs!", + // Loaded with cache-busting query strings so configuration fallback tests + // get independent module initialization. + "test/helpers/config/bundled-channel-config-runtime.ts!", + // The topology analyzer owns these as an intentionally self-contained graph. + "test/fixtures/ts-topology/basic/**/*.{js,mjs,cjs,ts,mts,cts}!", +] as const; + +const workspaces = Object.fromEntries( + Object.entries(productionConfig.workspaces).map(([workspace, settings]) => [ + workspace, + { + ...settings, + ...(workspace === "." + ? { + // Root test aliases and mocks load these from their owning bundled + // plugin workspaces; do not duplicate plugin runtime dependencies. + ignoreDependencies: [ + ...("ignoreDependencies" in settings ? settings.ignoreDependencies : []), + "baileys", + "discord-api-types", + ], + } + : {}), + entry: [ + ...settings.entry, + ...(workspace === "." + ? [".agents/skills/**/scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", ...ROOT_TEST_ENTRY_GLOBS] + : [TEST_ENTRY_GLOB]), + ], + project: + workspace === "." + ? [...settings.project, ".agents/skills/**/scripts/**/*.{js,mjs,cjs,ts,mts,cts}!"] + : settings.project, + }, + ]), +); + +const config = { + ...productionConfig, + ignoreFiles: [ + // The production pass omits scripts, live probes, and generated runtime + // surfaces. This test-aware pass audits all source files; only build output + // and ambient declarations stay excluded. + "dist/**", + "packages/*/dist/**", + "scripts/**/*.d.{mts,ts}", + // Ambient declarations and handwritten declaration companions are type + // inputs, not executable roots. + "test/external-script-modules.d.ts", + "test/vitest/**/*.d.{mts,ts}", + ], + // Keep only build artifacts out of the full-tree export audit. In + // particular, do not inherit production's test-support exclusions. + ignore: ["dist/**", "packages/*/dist/**", "**/.boundary-stubs/**"], + // This fixture deliberately mixes used, aliased, and unused exports so the + // topology analyzer can prove each classification. + ignoreIssues: { + // Cache-busting dynamic imports are real consumers, but Knip cannot map + // their query-suffixed module ids back to these named test-support exports. + "test/helpers/config/bundled-channel-config-runtime.ts": ["exports"], + "test/fixtures/ts-topology/basic/**": [ + "exports", + "nsExports", + "types", + "nsTypes", + "enumMembers", + "namespaceMembers", + ], + }, + workspaces, +}; + +export default config; diff --git a/config/knip.config.ts b/config/knip.config.ts index 678b34d15208..7c41a79f08ee 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -7,7 +7,77 @@ function bundledPluginFile(pluginId: string, relativePath: string, suffix = ""): return `${BUNDLED_PLUGIN_ROOT_DIR}/${pluginId}/${relativePath}${suffix}`; } +// Package scripts, workflows, Docker scenarios, and documented maintainer commands invoke these +// files by path. They are executable roots rather than importable library modules. +const repositoryScriptEntries = [ + "scripts/build-discord-activity-sdk.mjs!", + "scripts/check-live-cache.ts!", + "scripts/check-package-dist-imports.mjs!", + "scripts/dev/ios-node-e2e.ts!", + "scripts/diffs-shiki-curated.ts!", + "scripts/e2e/lib/browser-cdp-snapshot/assert-snapshot.mjs!", + "scripts/e2e/lib/browser-cdp-snapshot/fixture-server.mjs!", + "scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs!", + "scripts/e2e/lib/clawhub-fixture-server.cjs!", + "scripts/e2e/lib/codex-media-path/client.mjs!", + "scripts/e2e/lib/codex-media-path/fake-codex-app-server.mjs!", + "scripts/e2e/lib/codex-media-path/write-config.mjs!", + "scripts/e2e/lib/config-reload/assert-log.mjs!", + "scripts/e2e/lib/config-reload/mutate-metadata.mjs!", + "scripts/e2e/lib/docker-artifact-proof/write-identities.ts!", + "scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs!", + "scripts/e2e/lib/doctor-install-switch/write-wrapper.mjs!", + "scripts/e2e/lib/fixture.mjs!", + "scripts/e2e/lib/fixtures/config.mjs!", + "scripts/e2e/lib/fixtures/plugins.mjs!", + "scripts/e2e/lib/fixtures/workspace.mjs!", + "scripts/e2e/lib/npm-telegram-live/prepare-package.mjs!", + "scripts/e2e/lib/onboard/assert-config.mjs!", + "scripts/e2e/lib/onboard/write-config.mjs!", + "scripts/e2e/lib/openai-chat-tools/client.mjs!", + "scripts/e2e/lib/openai-chat-tools/write-config.mjs!", + "scripts/e2e/lib/package-git-fixture.mjs!", + "scripts/e2e/lib/parallels-package/build-info-commit.mjs!", + "scripts/e2e/lib/parallels-package/log-progress-extract.mjs!", + "scripts/e2e/lib/plugin-lifecycle-matrix/measure.mjs!", + "scripts/e2e/lib/plugin-update/registry-server.mjs!", + "scripts/e2e/lib/plugins/npm-registry-server.mjs!", + "scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs!", + "scripts/e2e/lib/release-scenarios/write-marketplace.mjs!", + "scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs!", + "scripts/e2e/lib/release-user-journey/write-clickclack-plugin.mjs!", + "scripts/e2e/lib/run-with-pty.mjs!", + "scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs!", + "scripts/embedded-run-abort-leak.ts!", + "scripts/fixtures/packed-plugin-sdk-type-smoke.ts!", + "scripts/ios-release-signing.mjs!", + "scripts/lib/docker-plugin-selection.mjs!", + "scripts/lib/openclaw-test-state.mjs!", + "scripts/list-prod-store-packages.mjs!", + // Invoked by scripts/lib/live-docker-stage.sh during container validation. + "scripts/live-docker-normalize-config.ts!", + "scripts/mcp-code-mode-gateway-e2e.ts!", + "scripts/openclaw-release-clawhub-plan.ts!", + "scripts/openclaw-release-clawhub-runtime-state.ts!", + "scripts/plugin-prerelease-liveish-matrix.mjs!", + "scripts/pr-gates-lock.mjs!", + "scripts/pr-lib/process-group-runner.mjs!", + "scripts/pre-commit/filter-staged-files.mjs!", + "scripts/qa-coverage-report.ts!", + "scripts/qa-parity-report.ts!", + "scripts/repro/tsx-name-repro.ts!", + "scripts/resolve-frozen-codex-live-suite.mjs!", + "scripts/secrets/openclaw-bws-resolver.mjs!", + "scripts/sync-labels.ts!", + "scripts/test-built-bundled-channel-entry-smoke.mjs!", + "scripts/update-clawtributors.ts!", + "scripts/verify-stable-main-closeout.mjs!", + "scripts/write-package-dist-inventory.ts!", + "scripts/write-plugin-sdk-entry-dts.ts!", +] as const; + const rootEntries = [ + ...repositoryScriptEntries, "openclaw.mjs!", "src/index.ts!", "src/entry.ts!", @@ -17,8 +87,16 @@ const rootEntries = [ "src/agents/compaction-planning.worker.ts!", "scripts/print-cli-backend-live-metadata.ts!", "scripts/repro/code-mode-namespace-live.ts!", + // Workflow/package-script entrypoints are not imported from production modules. + "scripts/openclaw-cross-os-release-checks.ts!", + "scripts/bench-sqlite-reliability.ts!", + // Docker/manual E2E executables and their nested assertion/probe entrypoints. + "scripts/e2e/*.{js,mjs,ts}!", + "scripts/e2e/lib/**/{assertions,probe,mock-server}.{js,mjs,ts}!", "src/audit/audit-event-writer.worker.ts!", "src/agents/model-provider-auth.worker.ts!", + // Split runtime loaded through a path assembled in subagent-registry.ts. + "src/agents/subagent-registry.runtime.ts!", // Loaded lazily by the registry; its callbacks form the orphan-recovery runtime contract. "src/agents/subagent-orphan-recovery.ts!", // Task cancellation loads this control facade by string path to avoid a registry cycle. @@ -38,6 +116,19 @@ const rootEntries = [ "src/mcp/codex-supervision-tools-serve.ts!", // Spawned by generated system-agent MCP configs; this stdio entry is not statically imported. "src/mcp/openclaw-tools-serve.ts!", + // Spawned by ACPX and QA Lab from a generated plugin-tool MCP command line. + "src/mcp/plugin-tools-serve.ts!", + // Dedicated tsdown entry exercised against built plugin singletons. + "src/plugins/build-smoke-entry.ts!", + // Package-script owners invoke these generated-artifact modules directly. + "src/config/doc-baseline.ts!", + "src/plugins/runtime-sidecar-paths-baseline.ts!", + // Imported by scripts/tsdown-build.mjs as the AI package build configuration. + "tsdown.ai.config.ts!", + // Maintainer-owned compatibility data referenced by release/docs workflows. + "src/commands/doctor/shared/deprecation-compat.ts!", + // Compiled as the package-boundary failure canary by the extension checker. + "src/plugins/contracts/rootdir-boundary-canary.ts!", "scripts/qa/render-maturity-docs.ts!", bundledPluginFile("telegram", "src/audit.ts", "!"), bundledPluginFile("telegram", "src/token.ts", "!"), @@ -49,7 +140,6 @@ const rootEntries = [ ] as const; const bundledPluginEntries = [ - "*.ts!", "index.ts!", "setup-entry.ts!", // Core resolves these public plugin artifacts by basename rather than by a @@ -57,6 +147,15 @@ const bundledPluginEntries = [ "*-api.ts!", "cli-metadata.ts!", "channel-entry.ts!", + // Manifest and SDK loaders resolve these public artifacts by basename. + "configured-state.ts!", + "auth-presence.ts!", + "thread-bindings-runtime.ts!", + "document-extractor.ts!", + "web-content-extractor.ts!", + "timeouts.ts!", + "action-runtime.runtime.ts!", + "allow-from.ts!", // Provider catalogs and web tools resolve these manifest/convention-owned // modules from the plugin root at runtime. "provider-discovery.ts!", @@ -67,8 +166,6 @@ const bundledPluginEntries = [ "src/subagent-hooks-api.ts!", ] as const; -const strictBundledPluginEntries = bundledPluginEntries.filter((entry) => entry !== "*.ts!"); - const bundledPluginIgnoredRuntimeDependencies = [ "@agentclientprotocol/claude-agent-acp", "@a2ui/lit", @@ -113,10 +210,27 @@ const rootBundledPluginRuntimeDependencies = [ "tokenjuice", ] as const; -function strictBundledPluginWorkspace(extraEntries: readonly string[] = []) { +// Root installation and build workflows deliberately mirror these dependencies from their +// owning workspace, or invoke their package binaries/loaders without a static module import. +const rootToolingAndWorkspaceDependencies = [ + "@a2ui/lit", + "@copilotkit/aimock", + "@lit-labs/signals", + "@lit/context", + // scripts/ui.js anchors these lookups at ui/package.json before invoking the UI workspace. + "@vitest/browser-playwright", + "dompurify", + "jscpd", + "lit", + "oxlint", + "oxlint-tsgolint", + "signal-utils", +] as const; + +function bundledPluginWorkspace(extraEntries: readonly string[] = []) { return { - entry: [...strictBundledPluginEntries, ...extraEntries], - project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], + entry: [...bundledPluginEntries, ...extraEntries], + project: ["**/*.{js,mjs,ts}!"], ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, } as const; } @@ -125,6 +239,7 @@ function strictBundledPluginWorkspace(extraEntries: readonly string[] = []) { // available to tests without becoming part of the production dead-code scan. const ignoredTestSupportFiles = [ "**/__tests__/**", + "**/test/**", "src/test-utils/**", "**/test-helpers/**", "**/test-fixtures/**", @@ -136,6 +251,9 @@ const ignoredTestSupportFiles = [ "**/*test-harness.ts", "**/*test-utils.ts", "**/*test-support.ts", + "**/*.test-loader.ts", + "**/*.live-helpers.ts", + "**/*.live-probe-helpers.ts", "**/*test-shared.ts", "**/*mocks.ts", "**/*.e2e-mocks.ts", @@ -169,13 +287,20 @@ const ignoredTestSupportFiles = [ "**/*.test-mocks.ts", "**/*.test-utils.ts", "test/helpers/live-image-probe.ts", + // Legacy test-only owners whose filenames predate the test-support convention. + "src/plugins/contracts/host-hook-fixture.ts", + "src/plugins/contracts/tts-contract-suites.ts", ] as const; const config = { ignoreFiles: [ + // Production mode excludes dev/maintainer executables. The full-tree + // companion config removes this exclusion and audits them as script roots. "scripts/**", "dist/**", "packages/*/dist/**", + // Declaration companions describe executable JavaScript modules; they are not standalone roots. + "scripts/**/*.d.{mts,ts}", "**/live-*.ts", "src/secrets/credential-matrix.ts", "src/shared/text/assistant-visible-text.ts", @@ -184,10 +309,14 @@ const config = { ], // Knip's `ignoreFiles` only suppresses unused-file findings. Test helpers // belong in `ignore` so they do not inflate unused-export/type findings. - ignore: ["dist/**", "packages/*/dist/**", ...ignoredTestSupportFiles], + ignore: ["dist/**", "packages/*/dist/**", "**/.boundary-stubs/**", ...ignoredTestSupportFiles], + // Script exports are checked with every script as an entry and entry-export + // reporting enabled. Suppress them only in this application-production scan. + ignoreIssues: { + "scripts/**": ["exports", "nsExports", "types", "nsTypes", "enumMembers", "namespaceMembers"], + }, workspaces: { ".": { - entry: rootEntries, ignoreDependencies: [ "@openclaw/*", // Docker packaging stages @openclaw/ai without nested dependencies after @@ -202,14 +331,19 @@ const config = { "partial-json", "sqlite-vec", "tree-sitter-bash", + ...rootToolingAndWorkspaceDependencies, ...rootBundledPluginRuntimeDependencies, ], + // Platform tools and shell builtins used by package scripts and process-boundary tests. + ignoreBinaries: ["mint", "open", "sleep", "xcrun"], project: [ "src/**/*.ts!", "scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + "test/**/*.{js,mjs,cjs,ts,mts,cts}!", "*.config.{js,mjs,cjs,ts,mts,cts}!", "*.mjs!", ], + entry: [...rootEntries, "test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!"], }, ui: { entry: [ @@ -372,13 +506,17 @@ const config = { entry: ["index.js!", "scripts/postinstall.js!"], project: ["index.js!", "scripts/**/*.js!"], }, - [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic-vertex`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/acpx`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/azure-speech`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/browser`]: strictBundledPluginWorkspace([ + [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic-vertex`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/acpx`]: bundledPluginWorkspace([ + // Copied as executable runtime internals by the package artifact manifest. + "src/runtime-internals/mcp-command-line.mjs!", + "src/runtime-internals/mcp-proxy.mjs!", + ]), + [`${BUNDLED_PLUGIN_ROOT_DIR}/azure-speech`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/browser`]: bundledPluginWorkspace([ // Core and plugin-SDK facades resolve these shipped Browser surfaces by basename. "browser-control-auth.ts!", "browser-config.ts!", @@ -386,53 +524,77 @@ const config = { "browser-host-inspection.ts!", "browser-maintenance.ts!", "browser-profiles.ts!", + // Chrome manifest/package scripts load these without TypeScript imports. + "chrome-extension/background.js!", + "chrome-extension/popup.js!", + "scripts/copy-chrome-extension.mjs!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/cloudflare-ai-gateway`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/chutes`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/clawrouter`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/cohere`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/comfy`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/copilot`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/copilot-proxy`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/codex`]: strictBundledPluginWorkspace([ - // Codex provider runtime and harness surfaces are reached through plugin + [`${BUNDLED_PLUGIN_ROOT_DIR}/canvas`]: bundledPluginWorkspace([ + // Package build/copy scripts are invoked from package.json. + "scripts/bundle-a2ui.mjs!", + "scripts/copy-a2ui.mjs!", + "scripts/pnpm-runner.mjs!", + // Rolldown consumes this config and its browser bootstrap entry. + "src/host/a2ui-app/rolldown.config.mjs!", + "src/host/a2ui-app/bootstrap.js!", + ]), + [`${BUNDLED_PLUGIN_ROOT_DIR}/cloudflare-ai-gateway`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/chutes`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/clawrouter`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/cohere`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/comfy`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/copilot`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/copilot-proxy`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/codex`]: bundledPluginWorkspace([ + // Provider runtime and harness surfaces are reached through plugin // registration contracts rather than static imports from the entrypoint. "harness.ts!", "media-understanding-provider.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/deepgram`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/deepinfra`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/discord`]: strictBundledPluginWorkspace([ - // Channel package-state probes resolve this module from package metadata. - "configured-state.ts!", + [`${BUNDLED_PLUGIN_ROOT_DIR}/deepgram`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/deepinfra`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/discord`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/diffs`]: bundledPluginWorkspace([ + // scripts/build-diffs-viewer-runtime.mjs bundles this browser entry. + "src/viewer-client.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/elevenlabs`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/featherless`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/fal`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/fireworks`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/google`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/huggingface`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/github-copilot`]: strictBundledPluginWorkspace([ + [`${BUNDLED_PLUGIN_ROOT_DIR}/elevenlabs`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/featherless`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/fal`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/fireworks`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/google`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/huggingface`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/github-copilot`]: bundledPluginWorkspace([ // Auth, replay, token, and stream helpers are runtime-owned provider - // surfaces that are consumed through plugin hooks and dynamic imports. + // surfaces consumed through plugin hooks and dynamic imports. "connection-bound-ids.ts!", "login.ts!", "stream.ts!", "token.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/kilocode`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/kimi-coding`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/microsoft`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/memory-core`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/memory-lancedb`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/microsoft-foundry`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/migrate-claude`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/migrate-hermes`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/minimax`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/mistral`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/moonshot`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/nvidia`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/openai`]: strictBundledPluginWorkspace([ + [`${BUNDLED_PLUGIN_ROOT_DIR}/kilocode`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/kimi-coding`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/matrix`]: bundledPluginWorkspace([ + // Native import wrapper shipped alongside the Matrix runtime bundle. + "src/plugin-entry.runtime.js!", + // The monitor lazy-loads outbound behavior on inbound-only processes. + "src/matrix/send.ts!", + ]), + [`${BUNDLED_PLUGIN_ROOT_DIR}/microsoft`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/memory-core`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/memory-lancedb`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/microsoft-foundry`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/migrate-claude`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/migrate-hermes`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/minimax`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/mistral`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/moonshot`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/mxc`]: bundledPluginWorkspace([ + // Copied to dist and spawned by the MXC backend. + "src/mxc-spawn-launcher.mjs!", + ]), + [`${BUNDLED_PLUGIN_ROOT_DIR}/nvidia`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/openai`]: bundledPluginWorkspace([ // OpenAI exposes provider, OAuth, overlay, media, usage, and realtime // contracts to runtime/plugin integration paths that Knip cannot trace. "embedding-batch.ts!", @@ -451,39 +613,47 @@ const config = { "tts.ts!", "usage.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/opencode`]: strictBundledPluginWorkspace([ + [`${BUNDLED_PLUGIN_ROOT_DIR}/opencode`]: bundledPluginWorkspace([ // Session catalog and provider helpers are plugin-owned runtime surfaces. "media-understanding-provider.ts!", "provider-catalog.ts!", "session-catalog-plugin.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/opencode-go`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/openrouter`]: strictBundledPluginWorkspace([ + [`${BUNDLED_PLUGIN_ROOT_DIR}/opencode-go`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/openrouter`]: bundledPluginWorkspace([ // OAuth, model, and media provider helpers are runtime/plugin surfaces. "image-generation-provider.ts!", "media-understanding-provider.ts!", "models.ts!", "oauth.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/pixverse`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/qianfan`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/qwen`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/qa-lab`]: strictBundledPluginWorkspace([ - // The plugin-SDK QA Lab facade resolves this CLI surface by basename. + [`${BUNDLED_PLUGIN_ROOT_DIR}/pixverse`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/qianfan`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/qwen`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/qa-lab`]: bundledPluginWorkspace([ + // Core loads the CLI facade by basename; QA Lab also owns a nested Vite app. "cli.ts!", - // The debugger UI is a separate browser entrypoint outside src/. + "web/index.html!", "web/src/app.ts!", + "web/src/main.ts!", + "web/vite.config.ts!", + // Imported directly from the GitHub Actions smoke-plan script. + "src/ci-smoke-plan.ts!", ]), - [`${BUNDLED_PLUGIN_ROOT_DIR}/senseaudio`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/tavily`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/tencent`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/vllm`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/voyage`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/xiaomi`]: strictBundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/xai`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/senseaudio`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/tavily`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/tencent`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/vllm`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/vault`]: bundledPluginWorkspace([ + // Shipped resolver child process declared as a static plugin artifact. + "vault-secret-ref-resolver.js!", + ]), + [`${BUNDLED_PLUGIN_ROOT_DIR}/voyage`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/xiaomi`]: bundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/xai`]: bundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/llama-cpp`]: { entry: bundledPluginEntries, - project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], + project: ["**/*.{js,mjs,ts}!"], ignoreDependencies: [ // The provider resolves node-llama-cpp from its own package at runtime // so local embeddings use the plugin-owned native dependency. @@ -491,21 +661,21 @@ const config = { ...bundledPluginIgnoredRuntimeDependencies, ], }, - [`${BUNDLED_PLUGIN_ROOT_DIR}/lmstudio`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/lmstudio`]: bundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/reef`]: { // Reef vendors its wire protocol under protocol/, which owns the noble // crypto dependencies. The protocol barrel is the vendored library's // public surface, so its exports are intentional even where the channel // consumes only a subset. entry: [...bundledPluginEntries, "protocol/index.ts!", "protocol/node.ts!"], - project: ["*.ts!", "src/**/*.{js,mjs,ts}!", "protocol/**/*.ts!"], + project: ["**/*.{js,mjs,ts}!"], ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, }, [`${BUNDLED_PLUGIN_ROOT_DIR}/*`]: { // Bundled plugins often load their public surface via string specifiers in // `index.ts` contracts, so Knip needs these convention-based entry files. entry: bundledPluginEntries, - project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], + project: ["**/*.{js,mjs,ts}!"], ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, }, }, diff --git a/config/knip.scripts-exports.config.ts b/config/knip.scripts-exports.config.ts new file mode 100644 index 000000000000..a6c214cfcbaa --- /dev/null +++ b/config/knip.scripts-exports.config.ts @@ -0,0 +1,76 @@ +/** + * Entry-export audit for repository scripts. + * + * Production configuration already owns the executable script roots. This + * companion pass keeps the rest of scripts/** as library project files and + * makes repository tests real consumers of deliberately testable helpers. + */ +import productionConfig from "./knip.config.ts"; + +const scriptEntries = productionConfig.workspaces["."].entry.filter((entry) => + entry.startsWith("scripts/"), +); + +const config = { + ignoreWorkspaces: ["apps/**", "extensions/**", "packages/**", "ui"], + ignore: ["scripts/**/*.d.{mts,cts,ts}", "scripts/**/*.test-support.{js,mjs,cjs,ts,mts,cts}"], + // Script entrypoints import core and Plugin SDK APIs. Those owners are + // checked by the application scans; this pass owns only scripts/** exports. + ignoreIssues: { + // These executable modules are also loaded through variable/file-URL imports + // by build or subprocess test harnesses, which Knip cannot resolve statically. + "scripts/diffs-shiki-curated.ts": [ + "exports", + "nsExports", + "types", + "nsTypes", + "enumMembers", + "namespaceMembers", + ], + "scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs": [ + "exports", + "nsExports", + "types", + "nsTypes", + "enumMembers", + "namespaceMembers", + ], + "scripts/e2e/secret-provider-integrations.mjs": [ + "exports", + "nsExports", + "types", + "nsTypes", + "enumMembers", + "namespaceMembers", + ], + "scripts/repro/code-mode-namespace-live.ts": [ + "exports", + "nsExports", + "types", + "nsTypes", + "enumMembers", + "namespaceMembers", + ], + "src/**": ["exports", "nsExports", "types", "nsTypes", "enumMembers", "namespaceMembers"], + "test/**": ["exports", "nsExports", "types", "nsTypes", "enumMembers", "namespaceMembers"], + }, + workspaces: { + ".": { + entry: [ + ...scriptEntries, + ".agents/skills/**/scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + "scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "src/plugin-sdk/api-baseline.ts!", + ], + project: [ + ".agents/skills/**/scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + "scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + "test/**/*.{js,mjs,cjs,ts,mts,cts}!", + "src/plugin-sdk/api-baseline.ts!", + ], + }, + }, +}; + +export default config; diff --git a/extensions/browser/chrome-extension/modules/relay-core.js b/extensions/browser/chrome-extension/modules/relay-core.js index 9fc84311fba2..f23222811c4a 100644 --- a/extensions/browser/chrome-extension/modules/relay-core.js +++ b/extensions/browser/chrome-extension/modules/relay-core.js @@ -4,7 +4,7 @@ /** Tab group shown to the user; membership == what the agent may touch. */ export const OPENCLAW_TAB_GROUP_TITLE = "OpenClaw"; -export const EXTENSION_RELAY_PROTOCOL = "openclaw-extension-relay"; +const EXTENSION_RELAY_PROTOCOL = "openclaw-extension-relay"; const EXTENSION_RELAY_TOKEN_PROTOCOL_PREFIX = "openclaw-extension-token."; const CHROME_GROUP_COLORS = { diff --git a/extensions/browser/test-support.ts b/extensions/browser/test-support.ts index c465dd1c7c40..a0bc474cd19b 100644 --- a/extensions/browser/test-support.ts +++ b/extensions/browser/test-support.ts @@ -8,17 +8,10 @@ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; export { createCliRuntimeCapture, expectGeneratedTokenPersistedToGatewayAuth, - type CliMockOutputRuntime, type CliRuntimeCapture, } from "openclaw/plugin-sdk/test-fixtures"; -export { - createTempHomeEnv, - withEnv, - withEnvAsync, - withFetchPreconnect, - isLiveTestEnabled, -} from "openclaw/plugin-sdk/test-env"; -export type { FetchMock, TempHomeEnv } from "openclaw/plugin-sdk/test-env"; +export { createTempHomeEnv, isLiveTestEnabled } from "openclaw/plugin-sdk/test-env"; +export type { TempHomeEnv } from "openclaw/plugin-sdk/test-env"; export type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; export function useAutoCleanupTempDirTracker(registerCleanup: (cleanup: () => void) => unknown) { diff --git a/extensions/codex/src/app-server/run-attempt-test-harness.ts b/extensions/codex/src/app-server/run-attempt-test-harness.ts index 21a4808694c6..0e642412d906 100644 --- a/extensions/codex/src/app-server/run-attempt-test-harness.ts +++ b/extensions/codex/src/app-server/run-attempt-test-harness.ts @@ -268,7 +268,7 @@ export function mockCall(mock: unknown, label: string, index = 0): unknown[] { return call; } -export function getMockServerVersion() { +function getMockServerVersion() { return "0.132.0"; } diff --git a/extensions/codex/src/app-server/session-binding.test-helpers.ts b/extensions/codex/src/app-server/session-binding.test-helpers.ts index 593b2ec18444..60b01a977647 100644 --- a/extensions/codex/src/app-server/session-binding.test-helpers.ts +++ b/extensions/codex/src/app-server/session-binding.test-helpers.ts @@ -127,10 +127,6 @@ export async function writeCodexAppServerBinding( await testCodexAppServerBindingStore.mutate(testIdentity(sessionId), { kind: "set", binding }); } -export async function clearCodexAppServerBinding(sessionId: string): Promise { - await testCodexAppServerBindingStore.mutate(testIdentity(sessionId), { kind: "clear" }); -} - export async function clearCodexAppServerBindingForThread( sessionId: string, threadId: string, diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index 315bbf8ad88b..66e2500466e7 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -78,20 +78,23 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({ resolveDefaultAgentDir: mocks.resolveDefaultAgentDir, })); +import { + assertCodexAppServerClientStartSelectionCurrent, + detachSharedCodexAppServerClientIfCurrent, + getSharedCodexAppServerClient, + readCodexAppServerClientProcessIdentity, +} from "./shared-client.js"; + let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels; let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient; -let assertCodexAppServerClientStartSelectionCurrent: typeof import("./shared-client.js").assertCodexAppServerClientStartSelectionCurrent; let clearSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrent; let clearSharedCodexAppServerClientIfCurrentAndWait: typeof import("./shared-client.js").clearSharedCodexAppServerClientIfCurrentAndWait; let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient; -let detachSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").detachSharedCodexAppServerClientIfCurrent; let getLeasedSharedCodexAppServerClient: typeof import("./shared-client.js").getLeasedSharedCodexAppServerClient; -let getSharedCodexAppServerClient: typeof import("./shared-client.js").getSharedCodexAppServerClient; let isCodexAppServerStartSelectionChangedError: typeof import("./shared-client.js").isCodexAppServerStartSelectionChangedError; let retainSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").retainSharedCodexAppServerClientIfCurrent; let releaseLeasedSharedCodexAppServerClient: typeof import("./shared-client.js").releaseLeasedSharedCodexAppServerClient; let releaseCodexAppServerClientLease: typeof import("./shared-client.js").releaseCodexAppServerClientLease; -let readCodexAppServerClientProcessIdentity: typeof import("./shared-client.js").readCodexAppServerClientProcessIdentity; let resolveCodexNativeConfigFenceKey: typeof import("./shared-client.js").resolveCodexNativeConfigFenceKey; let resolveCodexAppServerSpawnIdentity: typeof import("./shared-client.js").resolveCodexAppServerSpawnIdentity; let retireSharedCodexAppServerClientIfCurrent: typeof import("./shared-client.js").retireSharedCodexAppServerClientIfCurrent; @@ -183,19 +186,15 @@ describe("shared Codex app-server client", () => { beforeAll(async () => { ({ listCodexAppServerModels } = await import("./models.js")); ({ - assertCodexAppServerClientStartSelectionCurrent, clearSharedCodexAppServerClient, clearSharedCodexAppServerClientIfCurrent, clearSharedCodexAppServerClientIfCurrentAndWait, createIsolatedCodexAppServerClient, - detachSharedCodexAppServerClientIfCurrent, getLeasedSharedCodexAppServerClient, - getSharedCodexAppServerClient, isCodexAppServerStartSelectionChangedError, retainSharedCodexAppServerClientIfCurrent, releaseLeasedSharedCodexAppServerClient, releaseCodexAppServerClientLease, - readCodexAppServerClientProcessIdentity, resolveCodexNativeConfigFenceKey, resolveCodexAppServerSpawnIdentity, retireSharedCodexAppServerClientIfCurrent, diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index 17c01406209a..2a971acc633f 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -68,7 +68,7 @@ type CodexAppServerClientStartMetadata = { }; /** Successful physical process identity, excluding environment and credentials. */ -export type CodexAppServerClientProcessIdentity = { +type CodexAppServerClientProcessIdentity = { clientId: string; command: string; argsFingerprint: string; @@ -79,7 +79,7 @@ export type CodexAppServerClientProcessIdentity = { userAgent?: string; }; -export type CodexAppServerSpawnIdentity = Omit< +type CodexAppServerSpawnIdentity = Omit< CodexAppServerClientProcessIdentity, "clientId" | "serverVersion" | "userAgent" >; @@ -160,7 +160,7 @@ export function resolveCodexAppServerSpawnIdentity( }; } -export class CodexAppServerStartSelectionChangedError extends Error { +class CodexAppServerStartSelectionChangedError extends Error { readonly code = "CODEX_APP_SERVER_START_SELECTION_CHANGED"; constructor() { diff --git a/extensions/copilot/package.json b/extensions/copilot/package.json index d83b808a36b7..68b9bfca6a47 100644 --- a/extensions/copilot/package.json +++ b/extensions/copilot/package.json @@ -11,7 +11,6 @@ "@github/copilot-sdk": "1.0.5" }, "devDependencies": { - "@github/copilot": "1.0.68", "@openclaw/plugin-sdk": "workspace:*" }, "openclaw": { diff --git a/extensions/matrix/src/test-helpers.ts b/extensions/matrix/src/test-helpers.ts deleted file mode 100644 index 4c6c13daea4c..000000000000 --- a/extensions/matrix/src/test-helpers.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Matrix helper module supports test helpers behavior. -import fs from "node:fs"; -import path from "node:path"; - -export const MATRIX_TEST_HOMESERVER = "https://matrix.example.org"; -export const MATRIX_DEFAULT_USER_ID = "@bot:example.org"; -export const MATRIX_DEFAULT_ACCESS_TOKEN = "tok-123"; -export const MATRIX_DEFAULT_DEVICE_ID = "DEVICE123"; -export const MATRIX_OPS_ACCOUNT_ID = "ops"; -export const MATRIX_OPS_USER_ID = "@ops-bot:example.org"; -export const MATRIX_OPS_ACCESS_TOKEN = "tok-ops"; -export const MATRIX_OPS_DEVICE_ID = "DEVICEOPS"; - -export function writeFile(filePath: string, value: string) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, value, "utf8"); -} - -export function writeMatrixCredentials( - stateDir: string, - params?: { - accountId?: string; - homeserver?: string; - userId?: string; - accessToken?: string; - deviceId?: string; - }, -) { - const accountId = params?.accountId ?? MATRIX_OPS_ACCOUNT_ID; - writeFile( - path.join(stateDir, "credentials", "matrix", `credentials-${accountId}.json`), - JSON.stringify( - { - homeserver: params?.homeserver ?? MATRIX_TEST_HOMESERVER, - userId: params?.userId ?? MATRIX_OPS_USER_ID, - accessToken: params?.accessToken ?? MATRIX_OPS_ACCESS_TOKEN, - deviceId: params?.deviceId ?? MATRIX_OPS_DEVICE_ID, - }, - null, - 2, - ), - ); -} diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index b675ec809773..f0ac452811c7 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -15,8 +15,10 @@ "typebox": "1.3.3" }, "devDependencies": { + "@microsoft/teams.common": "2.0.13", "@openclaw/plugin-sdk": "workspace:*", "jose": "6.2.3", + "jwks-rsa": "3.2.2", "openclaw": "workspace:*" }, "peerDependencies": { diff --git a/extensions/nostr/src/test-fixtures.ts b/extensions/nostr/src/test-fixtures.ts index 15ea649d6f08..f2c7bbf1b1ee 100644 --- a/extensions/nostr/src/test-fixtures.ts +++ b/extensions/nostr/src/test-fixtures.ts @@ -7,8 +7,6 @@ export const TEST_HEX_PRIVATE_KEY = export const TEST_HEX_PUBLIC_KEY = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"; -export const TEST_NSEC = "nsec1qypqxpq9qtpqscx7peytzfwtdjmcv0mrz5rjpej8vjppfkqfqy8skqfv3l"; - export const TEST_RELAY_URL = "wss://relay.example.com"; export const TEST_SETUP_RELAY_URLS = ["wss://relay.damus.io", "wss://relay.primal.net"]; export const TEST_RESOLVED_PRIVATE_KEY = "resolved-nostr-private-key"; diff --git a/extensions/nostr/test/setup.ts b/extensions/nostr/test/setup.ts deleted file mode 100644 index 92926dc4885d..000000000000 --- a/extensions/nostr/test/setup.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Test setup file for nostr extension -import { vi } from "vitest"; - -// Mock console.error to suppress noise in tests -vi.spyOn(console, "error").mockImplementation(() => {}); diff --git a/extensions/policy/src/doctor/register.test-harness.ts b/extensions/policy/src/doctor/register.test-harness.ts index 924a69761ef3..c23973191993 100644 --- a/extensions/policy/src/doctor/register.test-harness.ts +++ b/extensions/policy/src/doctor/register.test-harness.ts @@ -15,9 +15,9 @@ import { registerPolicyDoctorChecks } from "./register.js"; export let workspaceDir: string; -export let originalOpenClawHome: string | undefined; +let originalOpenClawHome: string | undefined; -export let originalOpenClawStateDir: string | undefined; +let originalOpenClawStateDir: string | undefined; export function cfgWithPolicy(settings: Record = {}): OpenClawConfig { return { diff --git a/extensions/qa-lab/package.json b/extensions/qa-lab/package.json index b1f2f079c980..3ba5e7e67018 100644 --- a/extensions/qa-lab/package.json +++ b/extensions/qa-lab/package.json @@ -22,7 +22,8 @@ "@openclaw/plugin-sdk": "workspace:*", "@openclaw/slack": "workspace:*", "@openclaw/whatsapp": "workspace:*", - "openclaw": "workspace:*" + "openclaw": "workspace:*", + "vite": "8.1.3" }, "peerDependencies": { "openclaw": ">=2026.7.2" diff --git a/extensions/qa-lab/src/ci-smoke-plan.ts b/extensions/qa-lab/src/ci-smoke-plan.ts index 488265cd8ddb..935c01b31277 100644 --- a/extensions/qa-lab/src/ci-smoke-plan.ts +++ b/extensions/qa-lab/src/ci-smoke-plan.ts @@ -13,7 +13,7 @@ const QA_SMOKE_CI_SCENARIO_IDS = new Set([ "system-agent-ring-zero-setup", "dreaming-shadow-trial-report", "gateway-smoke", - "luna-thinking-visibility-switch", + "model-switch-follow-up", "group-visible-reply-tool", "long-running-release-audit", "matrix-restart-resume", diff --git a/extensions/qa-lab/web/src/ui-render-capture-timeline-model.ts b/extensions/qa-lab/web/src/ui-render-capture-timeline-model.ts index 4edb5427b927..a7396dcf94fa 100644 --- a/extensions/qa-lab/web/src/ui-render-capture-timeline-model.ts +++ b/extensions/qa-lab/web/src/ui-render-capture-timeline-model.ts @@ -283,5 +283,3 @@ export function buildCaptureTimelineModel(params: { visibleTimelineLanes, }; } - -export type CaptureTimelineModel = ReturnType; diff --git a/extensions/qa-lab/web/src/ui-types.ts b/extensions/qa-lab/web/src/ui-types.ts index cd0ff40d5936..e2d89179374d 100644 --- a/extensions/qa-lab/web/src/ui-types.ts +++ b/extensions/qa-lab/web/src/ui-types.ts @@ -55,7 +55,7 @@ export type Message = { reactions: Array<{ emoji: string; senderId: string }>; }; -export type BusEvent = +type BusEvent = | { cursor: number; kind: "thread-created"; thread: Thread } | { cursor: number; kind: string; message?: Message; emoji?: string }; @@ -104,7 +104,7 @@ export type Bootstrap = { }; }; -export type ScenarioStep = { +type ScenarioStep = { name: string; status: "pass" | "fail" | "skip"; details?: string; @@ -120,7 +120,7 @@ export type ScenarioOutcome = { finishedAt?: string; }; -export type ScenarioRun = { +type ScenarioRun = { kind: "suite" | "self-check"; status: "idle" | "running" | "completed"; startedAt?: string; @@ -144,7 +144,7 @@ export type RunnerSelection = { scenarioIds: string[]; }; -export type RunnerSnapshot = { +type RunnerSnapshot = { status: "idle" | "running" | "completed" | "failed"; selection: RunnerSelection; startedAt?: string; @@ -171,7 +171,7 @@ export type OutcomesEnvelope = { run: ScenarioRun | null; }; -export type CaptureSessionSummary = { +type CaptureSessionSummary = { id: string; startedAt: number; endedAt?: number; @@ -226,12 +226,12 @@ export type CaptureQueryEnvelope = { rows: Array>; }; -export type CaptureObservedDimension = { +type CaptureObservedDimension = { value: string; count: number; }; -export type CaptureCoverageSummary = { +type CaptureCoverageSummary = { sessionId: string; totalEvents: number; unlabeledEventCount: number; @@ -263,13 +263,13 @@ export type CaptureStartupStatusEnvelope = { status: CaptureStartupStatus; }; -export type EvidenceStatus = QaEvidenceGalleryEntryView["status"]; +type EvidenceStatus = QaEvidenceGalleryEntryView["status"]; export type EvidenceArtifactView = QaEvidenceArtifactView; export type EvidenceEntryView = QaEvidenceGalleryEntryView; export type EvidenceProducerContextFile = QaEvidenceProducerContextFile; export type EvidenceMatrixCell = QaEvidenceMatrixCellView; export type EvidenceProducerContext = QaEvidenceProducerContext; -export type EvidenceGalleryModel = QaEvidenceGalleryModel; +type EvidenceGalleryModel = QaEvidenceGalleryModel; export type EvidenceEnvelope = { evidence: EvidenceGalleryModel | null; diff --git a/extensions/qqbot/src/engine/commands/slash-command-test-support.ts b/extensions/qqbot/src/engine/commands/slash-command-test-support.ts index 75b85b9eb3c6..a1ded90794ec 100644 --- a/extensions/qqbot/src/engine/commands/slash-command-test-support.ts +++ b/extensions/qqbot/src/engine/commands/slash-command-test-support.ts @@ -7,7 +7,7 @@ type RuntimeConfigApi = ReturnType>; -export type WrittenQQBotConfig = { +type WrittenQQBotConfig = { streaming?: unknown; accounts?: { default?: { streaming?: unknown } }; }; diff --git a/extensions/slack/src/monitor/media.ts b/extensions/slack/src/monitor/media.ts index 66c34340df41..4eb9c825276d 100644 --- a/extensions/slack/src/monitor/media.ts +++ b/extensions/slack/src/monitor/media.ts @@ -11,7 +11,7 @@ import { import pMap, { pMapSkip } from "p-map"; import { formatSlackFileReference } from "../file-reference.js"; import type { SlackAttachment, SlackFile } from "../types.js"; -export { MAX_SLACK_MEDIA_FILES, type SlackMediaResult } from "./media-types.js"; +export type { SlackMediaResult } from "./media-types.js"; import { MAX_SLACK_MEDIA_FILES, type SlackMediaResult } from "./media-types.js"; import { type FetchLike, fetchWithRuntimeDispatcher, saveRemoteMedia } from "./media.runtime.js"; import { logVerbose } from "./thread.runtime.js"; @@ -19,8 +19,6 @@ export { resetSlackThreadStarterCacheForTest, resolveSlackThreadHistory, resolveSlackThreadStarter, - type SlackThreadMessage, - type SlackThreadStarter, } from "./thread.js"; function isSlackHostname(hostname: string): boolean { diff --git a/extensions/slack/src/monitor/thread.ts b/extensions/slack/src/monitor/thread.ts index 2a52de520aac..2c0aa8ed6e8a 100644 --- a/extensions/slack/src/monitor/thread.ts +++ b/extensions/slack/src/monitor/thread.ts @@ -173,7 +173,7 @@ export function resetSlackThreadStarterCacheForTest(): void { THREAD_STARTER_CACHE.clear(); } -export type SlackThreadMessage = { +type SlackThreadMessage = { text: string; userId?: string; ts?: string; diff --git a/extensions/telegram/src/bot-message-dispatch.test-harness.ts b/extensions/telegram/src/bot-message-dispatch.test-harness.ts index b154a9879430..2ccc60e44837 100644 --- a/extensions/telegram/src/bot-message-dispatch.test-harness.ts +++ b/extensions/telegram/src/bot-message-dispatch.test-harness.ts @@ -121,36 +121,36 @@ export const deliverInboundReplyWithMessageSendContext = deliverInboundReplyWithMessageSendContextHoisted; export const emitInternalMessageSentHook = emitInternalMessageSentHookHoisted; export const recordOutboundMessageForPromptContext = recordOutboundMessageForPromptContextHoisted; -export const createForumTopicTelegram = createForumTopicTelegramHoisted; -export const deleteMessageTelegram = deleteMessageTelegramHoisted; -export const editForumTopicTelegram = editForumTopicTelegramHoisted; +const createForumTopicTelegram = createForumTopicTelegramHoisted; +const deleteMessageTelegram = deleteMessageTelegramHoisted; +const editForumTopicTelegram = editForumTopicTelegramHoisted; export const editMessageTelegram = editMessageTelegramHoisted; -export const reactMessageTelegram = reactMessageTelegramHoisted; +const reactMessageTelegram = reactMessageTelegramHoisted; export const sendMessageTelegram = sendMessageTelegramHoisted; -export const sendPollTelegram = sendPollTelegramHoisted; -export const sendStickerTelegram = sendStickerTelegramHoisted; -export const loadConfig = loadConfigHoisted; -export const readChannelAllowFromStore = readChannelAllowFromStoreHoisted; -export const upsertChannelPairingRequest = upsertChannelPairingRequestHoisted; -export const enqueueSystemEvent = enqueueSystemEventHoisted; -export const buildModelsProviderData = buildModelsProviderDataHoisted; -export const listSkillCommandsForAgents = listSkillCommandsForAgentsHoisted; +const sendPollTelegram = sendPollTelegramHoisted; +const sendStickerTelegram = sendStickerTelegramHoisted; +const loadConfig = loadConfigHoisted; +const readChannelAllowFromStore = readChannelAllowFromStoreHoisted; +const upsertChannelPairingRequest = upsertChannelPairingRequestHoisted; +const enqueueSystemEvent = enqueueSystemEventHoisted; +const buildModelsProviderData = buildModelsProviderDataHoisted; +const listSkillCommandsForAgents = listSkillCommandsForAgentsHoisted; export const createChannelMessageReplyPipeline = createChannelMessageReplyPipelineHoisted; -export const wasSentByBot = wasSentByBotHoisted; +const wasSentByBot = wasSentByBotHoisted; export const appendAssistantMirrorMessageByIdentity = appendAssistantMirrorMessageByIdentityHoisted; -export const getSessionEntry = getSessionEntryHoisted; +const getSessionEntry = getSessionEntryHoisted; export const loadSessionStore = loadSessionStoreHoisted; export const readLatestAssistantTextByIdentity = readLatestAssistantTextByIdentityHoisted; -export const resolveStorePath = resolveStorePathHoisted; +const resolveStorePath = resolveStorePathHoisted; export const generateTopicLabel = generateTopicLabelHoisted; export const describeStickerImage = describeStickerImageHoisted; -export const loadModelCatalog = loadModelCatalogHoisted; -export const findModelInCatalog = findModelInCatalogHoisted; -export const modelSupportsVision = modelSupportsVisionHoisted; -export const resolveAgentDir = resolveAgentDirHoisted; -export const resolveDefaultModelForAgent = resolveDefaultModelForAgentHoisted; -export const getAgentScopedMediaLocalRoots = getAgentScopedMediaLocalRootsHoisted; -export const resolveChunkMode = resolveChunkModeHoisted; +const loadModelCatalog = loadModelCatalogHoisted; +const findModelInCatalog = findModelInCatalogHoisted; +const modelSupportsVision = modelSupportsVisionHoisted; +const resolveAgentDir = resolveAgentDirHoisted; +const resolveDefaultModelForAgent = resolveDefaultModelForAgentHoisted; +const getAgentScopedMediaLocalRoots = getAgentScopedMediaLocalRootsHoisted; +const resolveChunkMode = resolveChunkModeHoisted; export const resolveMarkdownTableMode = resolveMarkdownTableModeHoisted; vi.mock("./draft-stream.js", () => ({ @@ -675,4 +675,4 @@ export function describeTelegramDispatch(name: string, registerTests: () => void }); } -export type { Bot, TelegramBotDeps }; +export type { TelegramBotDeps }; diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index d8819b1418b5..2c71c766b148 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -218,7 +218,7 @@ const menuSyncHoisted = vi.hoisted(() => ({ await bot.api.setMyCommands(commandsToRegister); }), })); -export const syncTelegramMenuCommands = menuSyncHoisted.syncTelegramMenuCommands; +const syncTelegramMenuCommands = menuSyncHoisted.syncTelegramMenuCommands; function parseModelRef(raw: string): { provider?: string; model: string } { const trimmed = raw.trim(); @@ -357,7 +357,7 @@ const grammySpies = vi.hoisted(() => ({ export const useSpy: MockFn<(arg: unknown) => void> = grammySpies.useSpy; export const middlewareUseSpy: AnyMock = grammySpies.middlewareUseSpy; export const onSpy: AnyMock = grammySpies.onSpy; -export const stopSpy: AnyMock = grammySpies.stopSpy; +const stopSpy: AnyMock = grammySpies.stopSpy; export const commandSpy: AnyMock = grammySpies.commandSpy; export const botCtorSpy: MockFn< (token: string, options?: { client?: { fetch?: typeof fetch }; botInfo?: unknown }) => void @@ -433,7 +433,7 @@ const runnerHoisted = vi.hoisted(() => ({ export const sequentializeSpy: AnyMock = runnerHoisted.sequentializeSpy; export let sequentializeKey: ((ctx: unknown) => string) | undefined; export const throttlerSpy: AnyMock = runnerHoisted.throttlerSpy; -export const telegramBotRuntimeForTest = { +const telegramBotRuntimeForTest = { Bot: class { api = { config: { use: grammySpies.useSpy }, diff --git a/extensions/telegram/src/bot.media.e2e-harness.ts b/extensions/telegram/src/bot.media.e2e-harness.ts index 5a606875b7c4..96bc4bb3a80c 100644 --- a/extensions/telegram/src/bot.media.e2e-harness.ts +++ b/extensions/telegram/src/bot.media.e2e-harness.ts @@ -165,7 +165,7 @@ function installTopicNameRuntimeForTest(): void { } as TelegramRuntime); } -export const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = { +const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = { Bot: class { api = apiStub; use = middlewareUseSpy; diff --git a/extensions/twitch/test/setup.ts b/extensions/twitch/test/setup.ts deleted file mode 100644 index fb391c471d57..000000000000 --- a/extensions/twitch/test/setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Vitest setup file for Twitch plugin tests. - * - * Re-exports the root test setup to avoid duplication. - */ - -export * from "../../../test/setup.js"; diff --git a/extensions/voice-call/src/manager.test-harness.ts b/extensions/voice-call/src/manager.test-harness.ts index 860f509c9be4..92fa0399e15c 100644 --- a/extensions/voice-call/src/manager.test-harness.ts +++ b/extensions/voice-call/src/manager.test-harness.ts @@ -92,7 +92,7 @@ function createVoiceCallStateRuntimeForTests(): VoiceCallStateRuntime["state"] { }; } -export function installVoiceCallStateRuntimeForTests(): void { +function installVoiceCallStateRuntimeForTests(): void { setVoiceCallStateRuntime({ state: createVoiceCallStateRuntimeForTests() }); } diff --git a/extensions/whatsapp/constants.ts b/extensions/whatsapp/constants.ts deleted file mode 100644 index 8ddc5eac140d..000000000000 --- a/extensions/whatsapp/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Whatsapp plugin module implements constants behavior. -export { DEFAULT_WEB_MEDIA_BYTES } from "./src/auto-reply/constants.js"; diff --git a/extensions/whatsapp/src/auto-reply.test-harness.ts b/extensions/whatsapp/src/auto-reply.test-harness.ts index 94454ae34f85..5de8575ee234 100644 --- a/extensions/whatsapp/src/auto-reply.test-harness.ts +++ b/extensions/whatsapp/src/auto-reply.test-harness.ts @@ -20,7 +20,6 @@ import { resetLoadConfigMock as _resetLoadConfigMock, } from "./test-helpers.js"; -export { createAcceptedWhatsAppSendResult } from "./inbound/send-result.test-helper.js"; export { resetLoadConfigMock, setLoadConfigMock, diff --git a/extensions/whatsapp/src/monitor-inbox.test-harness.ts b/extensions/whatsapp/src/monitor-inbox.test-harness.ts index 7059e3a6c1ca..0cd2d6fed6e2 100644 --- a/extensions/whatsapp/src/monitor-inbox.test-harness.ts +++ b/extensions/whatsapp/src/monitor-inbox.test-harness.ts @@ -7,7 +7,6 @@ import { resetLogger, setLoggerOverride } from "openclaw/plugin-sdk/runtime-env" import { afterEach, beforeEach, expect, vi } from "vitest"; import { loadConfigMock, - readAllowFromStoreMock as pairingReadAllowFromStoreMock, resetPairingSecurityMocks, upsertPairingRequestMock as pairingUpsertPairingRequestMock, } from "./pairing-security.test-harness.js"; @@ -30,10 +29,9 @@ export const DEFAULT_WEB_INBOX_CONFIG = { }, } as const; export const mockLoadConfig: typeof loadConfigMock = loadConfigMock; -export const readAllowFromStoreMock = pairingReadAllowFromStoreMock; export const upsertPairingRequestMock = pairingUpsertPairingRequestMock; -export type MockSock = { +type MockSock = { ev: EventEmitter; end: AnyMockFn; ws: { close: AnyMockFn }; diff --git a/extensions/whatsapp/targets.ts b/extensions/whatsapp/targets.ts deleted file mode 100644 index 0ad1f94a94e4..000000000000 --- a/extensions/whatsapp/targets.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Whatsapp plugin module implements targets behavior. -export { - isWhatsAppGroupJid, - isWhatsAppUserTarget, - normalizeWhatsAppTarget, -} from "./src/normalize-target.js"; diff --git a/package.json b/package.json index 73eee30d9211..2aa51b921257 100644 --- a/package.json +++ b/package.json @@ -1584,10 +1584,11 @@ "crabbox:run": "node scripts/crabbox-wrapper.mjs run", "crabbox:stop": "node scripts/crabbox-wrapper.mjs stop", "crabbox:warmup": "node scripts/crabbox-wrapper.mjs warmup", - "deadcode:dependencies": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --dependencies --no-config-hints", + "deadcode:dependencies": "pnpm deadcode:full", "deadcode:exports": "node scripts/check-deadcode-exports.mjs", + "deadcode:full": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --no-config-hints --exclude duplicates && pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.all-exports.config.ts --no-progress --reporter compact --no-config-hints --exclude duplicates", "deadcode:knip": "pnpm --config.minimum-release-age=0 dlx --package knip@6.8.0 knip --config config/knip.config.ts --production --no-progress --reporter compact --files --dependencies", - "deadcode:report": "pnpm deadcode:knip; pnpm deadcode:exports", + "deadcode:report": "pnpm deadcode:full; pnpm deadcode:exports", "deadcode:unused-files": "node scripts/check-deadcode-unused-files.mjs", "deps:root-ownership": "node scripts/root-dependency-ownership-audit.mjs", "deps:root-ownership:check": "node scripts/root-dependency-ownership-audit.mjs --check", @@ -2068,7 +2069,6 @@ "devDependencies": { "@a2ui/lit": "0.10.1", "@copilotkit/aimock": "1.35.0", - "@grammyjs/types": "3.28.0", "@lit-labs/signals": "0.3.0", "@lit/context": "1.1.6", "@mdx-js/mdx": "3.1.1", @@ -2086,19 +2086,23 @@ "@types/ws": "8.18.1", "@typescript/native-preview": "7.0.0-dev.20260707.2", "@vitest/coverage-v8": "4.1.9", + "acorn": "8.17.0", "esbuild": "0.28.1", + "fast-glob": "3.3.3", "jscpd": "4.2.4", "jsdom": "29.1.1", "lit": "3.3.3", "oxfmt": "0.58.0", "oxlint": "1.73.0", "oxlint-tsgolint": "0.24.0", + "playwright": "1.61.1", "shiki": "4.3.0", "signal-utils": "0.21.1", "sigstore": "4.1.1", "tsdown": "0.22.1", "tsx": "4.22.4", "unrun": "0.3.1", + "vite": "8.1.3", "vitest": "4.1.9" }, "optionalDependencies": { diff --git a/packages/ai/src/providers/google-shared.ts b/packages/ai/src/providers/google-shared.ts index bf0171197dff..6d3b4d860002 100644 --- a/packages/ai/src/providers/google-shared.ts +++ b/packages/ai/src/providers/google-shared.ts @@ -92,7 +92,7 @@ function isThinkingPart(part: Pick): boole * a signature from being overwritten with `undefined` within the same streamed block. * @internal Directly tested provider implementation detail. */ -export function retainThoughtSignature( +function retainThoughtSignature( existing: string | undefined, incoming: string | undefined, ): string | undefined { @@ -129,7 +129,7 @@ function resolveThoughtSignature( * Models via Google APIs that require explicit tool call IDs in function calls/responses. * @internal Directly tested provider implementation detail. */ -export function requiresToolCallId(modelId: string): boolean { +function requiresToolCallId(modelId: string): boolean { return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-"); } @@ -396,7 +396,7 @@ export function convertTools( * Map tool choice string to Gemini FunctionCallingConfigMode. * @internal Directly tested provider implementation detail. */ -export function mapToolChoice(choice: string): FunctionCallingConfigMode { +function mapToolChoice(choice: string): FunctionCallingConfigMode { switch (choice) { case "auto": return FunctionCallingConfigMode.AUTO; @@ -604,7 +604,7 @@ export function getDisabledGoogleThinkingConfig( } /** @internal Directly tested provider implementation detail. */ -export function isGemma4Model(model: Model): boolean { +function isGemma4Model(model: Model): boolean { return /gemma-?4/.test(model.id.toLowerCase()); } @@ -701,7 +701,7 @@ function getGoogleBudget( * Map Gemini FinishReason to our StopReason. * @internal Directly tested provider implementation detail. */ -export function mapStopReason(reason: FinishReason): StopReason { +function mapStopReason(reason: FinishReason): StopReason { switch (reason) { case FinishReason.STOP: return "stop"; diff --git a/packages/gateway-protocol/src/schema/artifacts.ts b/packages/gateway-protocol/src/schema/artifacts.ts index 59a4c602800c..0df655ef6b6a 100644 --- a/packages/gateway-protocol/src/schema/artifacts.ts +++ b/packages/gateway-protocol/src/schema/artifacts.ts @@ -18,10 +18,10 @@ const ArtifactQueryParamsProperties = { }; /** Shared artifact filter payload used by list-style requests. */ -export const ArtifactQueryParamsSchema = closedObject(ArtifactQueryParamsProperties); +const ArtifactQueryParamsSchema = closedObject(ArtifactQueryParamsProperties); /** Artifact lookup payload with a required artifact id plus optional scope filters. */ -export const ArtifactGetParamsSchema = closedObject({ +const ArtifactGetParamsSchema = closedObject({ ...ArtifactQueryParamsProperties, artifactId: NonEmptyString, }); diff --git a/packages/memory-host-sdk/package.json b/packages/memory-host-sdk/package.json index 7a75838808e7..457d4813f84b 100644 --- a/packages/memory-host-sdk/package.json +++ b/packages/memory-host-sdk/package.json @@ -8,6 +8,9 @@ "@openclaw/retry": "workspace:*", "p-map": "7.0.5" }, + "devDependencies": { + "openclaw": "workspace:*" + }, "exports": { "./runtime-core": "./src/runtime-core.ts", "./runtime-cli": "./src/runtime-cli.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc92745737b1..0993dd8fbd91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -236,9 +236,6 @@ importers: '@copilotkit/aimock': specifier: 1.35.0 version: 1.35.0(vitest@4.1.9) - '@grammyjs/types': - specifier: 3.28.0 - version: 3.28.0 '@lit-labs/signals': specifier: 0.3.0 version: 0.3.0 @@ -290,9 +287,15 @@ importers: '@vitest/coverage-v8': specifier: 4.1.9 version: 4.1.9(@vitest/browser@4.1.9)(vitest@4.1.9) + acorn: + specifier: 8.17.0 + version: 8.17.0 esbuild: specifier: 0.28.1 version: 0.28.1 + fast-glob: + specifier: 3.3.3 + version: 3.3.3 jscpd: specifier: 4.2.4 version: 4.2.4 @@ -311,6 +314,9 @@ importers: oxlint-tsgolint: specifier: 0.24.0 version: 0.24.0 + playwright: + specifier: 1.61.1 + version: 1.61.1 shiki: specifier: 4.3.0 version: 4.3.0 @@ -329,6 +335,9 @@ importers: unrun: specifier: 0.3.1 version: 0.3.1 + vite: + specifier: 8.1.3 + version: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) vitest: specifier: 4.1.9 version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.1.0)(@vitest/browser-playwright@4.1.9)(@vitest/coverage-v8@4.1.9)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -595,9 +604,6 @@ importers: specifier: 1.0.5 version: 1.0.5 devDependencies: - '@github/copilot': - specifier: 1.0.68 - version: 1.0.68 '@openclaw/plugin-sdk': specifier: workspace:* version: link:../../packages/plugin-sdk @@ -1260,12 +1266,18 @@ importers: specifier: 1.3.3 version: 1.3.3 devDependencies: + '@microsoft/teams.common': + specifier: 2.0.13 + version: 2.0.13 '@openclaw/plugin-sdk': specifier: workspace:* version: link:../../packages/plugin-sdk jose: specifier: 6.2.3 version: 6.2.3 + jwks-rsa: + specifier: 3.2.2 + version: 3.2.2 openclaw: specifier: workspace:* version: link:../.. @@ -1458,18 +1470,18 @@ importers: extensions/qa-lab: dependencies: - '@openclaw/crabline': - specifier: 0.1.11 - version: 0.1.11 - '@openclaw/matrix': - specifier: workspace:* - version: link:../matrix '@copilotkit/aimock': specifier: 1.35.0 version: 1.35.0(vitest@4.1.9) '@modelcontextprotocol/sdk': specifier: 1.29.0 version: 1.29.0(zod@4.4.3) + '@openclaw/crabline': + specifier: 0.1.11 + version: 0.1.11 + '@openclaw/matrix': + specifier: workspace:* + version: link:../matrix p-limit: specifier: 7.3.0 version: 7.3.0 @@ -1507,6 +1519,9 @@ importers: openclaw: specifier: workspace:* version: link:../.. + vite: + specifier: 8.1.3 + version: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) extensions/qianfan: devDependencies: @@ -2094,6 +2109,10 @@ importers: p-map: specifier: 7.0.5 version: 7.0.5 + devDependencies: + openclaw: + specifier: workspace:* + version: link:../.. packages/model-catalog-core: dependencies: @@ -2242,12 +2261,18 @@ importers: '@types/markdown-it': specifier: 14.1.2 version: 14.1.2 + '@vitest/browser': + specifier: 4.1.9 + version: 4.1.9(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(vitest@4.1.9) '@vitest/browser-playwright': specifier: 4.1.9 version: 4.1.9(playwright@1.61.1)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(vitest@4.1.9) jsdom: specifier: 29.1.1 version: 29.1.1(@noble/hashes@2.2.0) + openclaw: + specifier: workspace:* + version: link:.. playwright: specifier: 1.61.1 version: 1.61.1 diff --git a/scripts/check-deadcode-exports.mjs b/scripts/check-deadcode-exports.mjs index dca7302cbb95..36fdc34fd2d4 100644 --- a/scripts/check-deadcode-exports.mjs +++ b/scripts/check-deadcode-exports.mjs @@ -3,15 +3,29 @@ import { fileURLToPath } from "node:url"; import { isLikelyRepoFilePath, runKnip, uniqueSorted } from "./deadcode-knip-runner.mjs"; -const KNIP_ARGS = [ - "--config", - "config/knip.config.ts", - "--production", +const KNIP_ISSUES = "exports,nsExports,types,nsTypes,enumMembers,namespaceMembers"; + +const KNIP_SCANS = [ + { + name: "production unused-export scan", + args: ["--config", "config/knip.config.ts", "--production"], + }, + { + name: "full-tree unused-export scan", + args: ["--config", "config/knip.all-exports.config.ts"], + }, + { + name: "script unused-export scan", + args: ["--config", "config/knip.scripts-exports.config.ts", "--include-entry-exports"], + }, +]; + +const KNIP_COMMON_ARGS = [ "--no-progress", "--reporter", "compact", "--include", - "exports,nsExports,types,nsTypes,enumMembers,namespaceMembers", + KNIP_ISSUES, "--no-config-hints", ]; @@ -82,18 +96,30 @@ export function checkUnusedExports(output) { } async function main() { - const result = await runKnip(KNIP_ARGS, { scanName: "unused-export scan" }); + for (const scan of KNIP_SCANS) { + const ok = await runUnusedExportScan(scan); + if (!ok) { + process.exitCode = 1; + return; + } + } + console.log( + "[deadcode] Knip production and full-tree unused-export checks passed with 0 entries.", + ); +} + +async function runUnusedExportScan(scan) { + const result = await runKnip([...scan.args, ...KNIP_COMMON_ARGS], { scanName: scan.name }); if (result.errorCode || result.status === null) { console.error( - `deadcode unused-export scan failed: ${result.errorCode ?? result.signal ?? "unknown"}${ + `deadcode ${scan.name} failed: ${result.errorCode ?? result.signal ?? "unknown"}${ result.errorMessage ? `: ${result.errorMessage}` : "" }`, ); if (result.output) { console.error(result.output); } - process.exitCode = 1; - return; + return false; } const parsed = parseKnipCompactUnusedExportsResult(result.output); @@ -101,21 +127,27 @@ async function main() { // legitimately prints no export sections; sectionless output is only a // failure signal when Knip also exited nonzero (crash/config error). if (!parsed.sawExportSection && result.status !== 0) { - console.error("deadcode unused-export scan produced no export sections."); + console.error(`deadcode ${scan.name} produced no export sections.`); if (result.output) { console.error(result.output); } - process.exitCode = 1; - return; + return false; } const check = checkUnusedExports(result.output); if (!check.ok) { - console.error(check.message); - process.exitCode = 1; - return; + console.error(`${scan.name}:\n${check.message}`); + return false; } - console.log("[deadcode] Knip unused-export check passed with 0 entries."); + if (result.status !== 0) { + console.error(`deadcode ${scan.name} exited with status ${result.status}.`); + if (result.output) { + console.error(result.output); + } + return false; + } + console.log(`[deadcode] Knip ${scan.name} passed with 0 entries.`); + return true; } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/scripts/check-deadcode-unused-files.d.mts b/scripts/check-deadcode-unused-files.d.mts index 1ea9e0c91aac..bdac3a837f7a 100644 --- a/scripts/check-deadcode-unused-files.d.mts +++ b/scripts/check-deadcode-unused-files.d.mts @@ -1,47 +1,34 @@ #!/usr/bin/env node -/** - * Parses compact knip output into unused file paths. - */ -export function parseKnipCompactUnusedFiles(output: unknown): unknown[]; -/** - * Compares detected unused files against the checked-in allowlist. - */ -export function compareUnusedFilesToAllowlist( - actualFiles: unknown, - allowlistFiles: unknown, - optionalAllowlistFiles?: unknown[], -): { - actual: unknown[]; - allowed: unknown[]; - unexpected: unknown[]; - stale: unknown[]; - duplicateAllowedCount: number; - allowlistIsSorted: boolean; + +export type KnipUnusedFileScanResult = { + errorCode?: string; + errorMessage?: string; + output: string; + signal?: string | null; + status: number | null; }; -/** - * Runs knip and returns parsed unused-file results. - */ -export function runKnipUnusedFiles(params?: Record): Promise; -/** - * Checks detected unused files against the current allowlist. - */ -export function checkUnusedFiles( - output: unknown, - allowlistFiles?: string[], - optionalAllowlistFiles?: string[], -): { - ok: boolean; - comparison: { - actual: unknown[]; - allowed: unknown[]; - unexpected: unknown[]; - stale: unknown[]; - duplicateAllowedCount: number; - allowlistIsSorted: boolean; - }; + +/** Parses compact Knip output into unused file paths. */ +export function parseKnipCompactUnusedFiles(output: string): string[]; + +/** Runs the production Knip unused-file scan. */ +export function runKnipUnusedFiles( + params?: Record, +): Promise; + +/** Rejects every unused file reported by Knip. */ +export function checkUnusedFiles(output: string): { + files: string[]; message: string; + ok: boolean; }; -/** - * Maximum buffered knip output retained for diagnostics. - */ + +/** Validates both Knip process completion and the unused-file report. */ +export function checkKnipUnusedFileScanResult(result: KnipUnusedFileScanResult): { + failureReason: string; + message: string; + ok: boolean; +}; + +/** Maximum buffered Knip output retained for diagnostics. */ export const KNIP_MAX_BUFFER_BYTES: number; diff --git a/scripts/check-deadcode-unused-files.mjs b/scripts/check-deadcode-unused-files.mjs index 4d23e055591f..0971a7b05cb9 100644 --- a/scripts/check-deadcode-unused-files.mjs +++ b/scripts/check-deadcode-unused-files.mjs @@ -1,29 +1,26 @@ #!/usr/bin/env node -// Runs Knip unused-file detection and compares results to the allowlist. +// Enforces a hard-zero policy for Knip's unused files. import { fileURLToPath } from "node:url"; import { - compareStringListToAllowlist, isLikelyRepoFilePath, KNIP_MAX_BUFFER_BYTES, runKnip, uniqueSorted, } from "./deadcode-knip-runner.mjs"; -import { - KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST, - KNIP_UNUSED_FILE_ALLOWLIST, -} from "./deadcode-unused-files.allowlist.mjs"; export { KNIP_MAX_BUFFER_BYTES }; -const KNIP_ARGS = [ - "--config", - "config/knip.config.ts", - "--production", - "--no-progress", - "--reporter", - "compact", - "--files", - "--no-config-hints", +const KNIP_COMMON_ARGS = ["--no-progress", "--reporter", "compact", "--files", "--no-config-hints"]; + +const KNIP_SCANS = [ + { + name: "production unused-file scan", + args: ["--config", "config/knip.config.ts", "--production"], + }, + { + name: "full-tree unused-file scan", + args: ["--config", "config/knip.all-exports.config.ts"], + }, ]; /** Parses compact Knip output into unused file paths. */ @@ -55,88 +52,77 @@ export function parseKnipCompactUnusedFiles(output) { return uniqueSorted(files); } -/** Compares detected unused files against the checked-in allowlist. */ -export function compareUnusedFilesToAllowlist( - actualFiles, - allowlistFiles, - optionalAllowlistFiles = [], -) { - return compareStringListToAllowlist(actualFiles, allowlistFiles, optionalAllowlistFiles); -} - -function formatUnusedFileComparison(comparison) { - const lines = []; - if (!comparison.allowlistIsSorted) { - lines.push("deadcode unused-file allowlist is not sorted."); - } - if (comparison.duplicateAllowedCount > 0) { - lines.push( - `deadcode unused-file allowlist contains ${comparison.duplicateAllowedCount} duplicate entr${ - comparison.duplicateAllowedCount === 1 ? "y" : "ies" - }.`, - ); - } - if (comparison.unexpected.length > 0) { - lines.push("Unexpected unused files:"); - lines.push(...comparison.unexpected.map((file) => ` ${file}`)); - } - if (comparison.stale.length > 0) { - lines.push("Stale allowlist entries:"); - lines.push(...comparison.stale.map((file) => ` ${file}`)); - } - return lines.join("\n"); -} - /** Runs Knip and returns parsed unused-file results. */ export async function runKnipUnusedFiles(params = {}) { - return await runKnip(KNIP_ARGS, { ...params, scanName: "unused-file scan" }); + return await runKnip([...KNIP_SCANS[0].args, ...KNIP_COMMON_ARGS], { + ...params, + scanName: KNIP_SCANS[0].name, + }); } -/** Checks detected unused files against the current allowlist. */ -export function checkUnusedFiles( - output, - allowlistFiles = KNIP_UNUSED_FILE_ALLOWLIST, - optionalAllowlistFiles = KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST, -) { - const actual = parseKnipCompactUnusedFiles(output); - const comparison = compareUnusedFilesToAllowlist(actual, allowlistFiles, optionalAllowlistFiles); +/** Rejects every unused file reported by Knip. */ +export function checkUnusedFiles(output) { + const files = parseKnipCompactUnusedFiles(output); return { - ok: - comparison.allowlistIsSorted && - comparison.duplicateAllowedCount === 0 && - comparison.unexpected.length === 0 && - comparison.stale.length === 0, - comparison, - message: formatUnusedFileComparison(comparison), + ok: files.length === 0, + files, + message: + files.length === 0 + ? "" + : [ + "Unused files are not allowed:", + ...files.map((file) => ` ${file}`), + "Delete the files or model their real entrypoints in Knip.", + ].join("\n"), }; } +/** Validates both Knip process completion and the unused-file report. */ +export function checkKnipUnusedFileScanResult(result) { + if (result.errorCode || result.status === null || result.status !== 0) { + return { + ok: false, + failureReason: result.errorCode ?? result.signal ?? `exit status ${String(result.status)}`, + message: "", + }; + } + const check = checkUnusedFiles(result.output); + return { ok: check.ok, failureReason: "", message: check.message }; +} + async function main() { - const result = await runKnipUnusedFiles(); - if (result.errorCode || result.status === null) { + for (const scan of KNIP_SCANS) { + const ok = await runUnusedFileScan(scan); + if (!ok) { + process.exitCode = 1; + return; + } + } + console.log("[deadcode] Knip production and full-tree unused-file checks passed with 0 entries."); +} + +async function runUnusedFileScan(scan) { + const result = await runKnip([...scan.args, ...KNIP_COMMON_ARGS], { scanName: scan.name }); + const validation = checkKnipUnusedFileScanResult(result); + if (validation.failureReason) { console.error( - `deadcode unused-file scan failed: ${result.errorCode ?? result.signal ?? "unknown"}${ + `deadcode ${scan.name} failed: ${validation.failureReason}${ result.errorMessage ? `: ${result.errorMessage}` : "" }`, ); if (result.output) { console.error(result.output); } - process.exitCode = 1; - return; + return false; } - const check = checkUnusedFiles(result.output); - if (!check.ok) { - if (check.message) { - console.error(check.message); + if (!validation.ok) { + if (validation.message) { + console.error(`${scan.name}:\n${validation.message}`); } - process.exitCode = 1; - return; + return false; } - - console.log( - `[deadcode] Knip unused-file allowlist matched ${check.comparison.actual.length} intentional entries.`, - ); + console.log(`[deadcode] Knip ${scan.name} passed with 0 entries.`); + return true; } if (process.argv[1] === fileURLToPath(import.meta.url)) { diff --git a/scripts/check-duplicates.mjs b/scripts/check-duplicates.mjs index 772ae301f4b5..892ccc59a7c6 100644 --- a/scripts/check-duplicates.mjs +++ b/scripts/check-duplicates.mjs @@ -21,8 +21,8 @@ const targets = [ "security", "test", "skills", + "config", "openclaw.mjs", - "config/knip.config.ts", "tsdown.ai.config.ts", "tsdown.config.ts", "vitest.config.ts", diff --git a/scripts/deadcode-knip-runner.mjs b/scripts/deadcode-knip-runner.mjs index e1212828a663..2bfe4af45c20 100644 --- a/scripts/deadcode-knip-runner.mjs +++ b/scripts/deadcode-knip-runner.mjs @@ -11,7 +11,7 @@ const KNIP_HEARTBEAT_MS = 60_000; /** Maximum buffered Knip output retained for diagnostics. */ export const KNIP_MAX_BUFFER_BYTES = 16 * 1024 * 1024; -export function normalizeRepoPath(value) { +function normalizeRepoPath(value) { return value.replaceAll("\\", "/").replace(/^\.\//u, ""); } @@ -22,26 +22,9 @@ export function uniqueSorted(values) { } export function isLikelyRepoFilePath(value) { - return /^(apps|docs|extensions|packages|scripts|src|test|ui)\//u.test(normalizeRepoPath(value)); -} - -/** Compares a detected string list with required and optional baseline entries. */ -export function compareStringListToAllowlist(actualValues, allowlistValues, optionalValues = []) { - const actual = uniqueSorted(actualValues); - const allowed = uniqueSorted(allowlistValues); - const optionalAllowed = uniqueSorted(optionalValues); - const allowedOrOptionalSet = new Set([...allowed, ...optionalAllowed]); - const actualSet = new Set(actual); - - return { - actual, - allowed, - unexpected: actual.filter((value) => !allowedOrOptionalSet.has(value)), - stale: allowed.filter((value) => !actualSet.has(value)), - duplicateAllowedCount: allowlistValues.length - new Set(allowlistValues).size, - allowlistIsSorted: - JSON.stringify(allowlistValues.map(normalizeRepoPath)) === JSON.stringify(allowed), - }; + return /^(\.agents|apps|config|docs|extensions|packages|scripts|src|test|ui)\//u.test( + normalizeRepoPath(value), + ); } function spawnErrorCode(error) { diff --git a/scripts/deadcode-unused-files.allowlist.mjs b/scripts/deadcode-unused-files.allowlist.mjs deleted file mode 100644 index 15ab1df212ef..000000000000 --- a/scripts/deadcode-unused-files.allowlist.mjs +++ /dev/null @@ -1,40 +0,0 @@ -// Intentional Knip unused-file findings. These are dynamic entrypoints, -// generated/build inputs, manifest-discovered plugin surfaces, live-test -// helpers, or package bridge files that static production scanning cannot see. -export const KNIP_UNUSED_FILE_ALLOWLIST = ["extensions/qa-lab/src/ci-smoke-plan.ts"]; - -// Knip can disagree across supported local/CI platforms for files that are -// only reachable through test-only import graphs, sparse-checkout proof -// workspaces, dynamic entrypoints, manifest-discovered plugin surfaces, or -// package bridge files. Ignore these when reported, but do not require them -// to be reported. -export const KNIP_OPTIONAL_UNUSED_FILE_ALLOWLIST = [ - "extensions/acpx/src/runtime-internals/mcp-command-line.mjs", - "extensions/acpx/src/runtime-internals/mcp-proxy.mjs", - "extensions/canvas/src/host/a2ui-app/bootstrap.js", - "extensions/canvas/src/host/a2ui-app/rolldown.config.mjs", - "extensions/diffs/src/viewer-client.ts", - "extensions/diffs/src/viewer-payload.ts", - "extensions/matrix/src/plugin-entry.runtime.js", - "extensions/mxc/src/mxc-spawn-launcher.mjs", - "src/agents/subagent-registry.runtime.ts", - "src/auto-reply/reply/get-reply.test-loader.ts", - "src/commands/doctor/shared/deprecation-compat.ts", - "src/config/doc-baseline.runtime.ts", - "src/config/doc-baseline.ts", - "src/gateway/gateway-cli-backend.live-helpers.ts", - "src/gateway/gateway-cli-backend.live-probe-helpers.ts", - "src/gateway/gateway-codex-harness.live-helpers.ts", - "src/mcp/plugin-tools-handlers.ts", - "src/mcp/plugin-tools-serve.ts", - "src/mcp/tools-stdio-server.ts", - "src/plugins/build-smoke-entry.ts", - "src/plugins/contracts/host-hook-fixture.ts", - "src/plugins/contracts/rootdir-boundary-canary.ts", - "src/plugins/contracts/tts-contract-suites.ts", - "src/plugins/runtime-sidecar-paths-baseline.ts", - "src/tasks/task-registry-control.runtime.ts", - "extensions/qa-lab/src/auth-profile.fixture.ts", - "extensions/qa-lab/src/codex-plugin.fixture.ts", - "extensions/qa-lab/src/mantis-phase-timer.runtime.ts", -]; diff --git a/scripts/dev/gateway-smoke.ts b/scripts/dev/gateway-smoke.ts index c0df403d4fdc..1bc71594e192 100644 --- a/scripts/dev/gateway-smoke.ts +++ b/scripts/dev/gateway-smoke.ts @@ -234,8 +234,3 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { process.exitCode = await runGatewaySmoke({ token: cli.token, urlRaw: cli.urlRaw }); } } - -export const testing = { - parseGatewaySmokeCli, - usage, -}; diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs index a7f8bb8b0d5c..d807dc80a897 100644 --- a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs @@ -1524,7 +1524,7 @@ function tailText(text) { return text.split(/\r?\n/u).slice(-120).join("\n"); } -export async function main(argv = process.argv.slice(2)) { +async function main(argv = process.argv.slice(2)) { const [command, pluginId, pluginDir, requiresConfigRaw, pluginIndexRaw, pluginRoot, provider] = argv; const pluginIndex = readNonNegativeInt(pluginIndexRaw, 0, "bundled plugin runtime index"); diff --git a/scripts/e2e/lib/gateway-network/client.mjs b/scripts/e2e/lib/gateway-network/client.mjs index c4f99aa1987a..1680f1cf65e7 100644 --- a/scripts/e2e/lib/gateway-network/client.mjs +++ b/scripts/e2e/lib/gateway-network/client.mjs @@ -293,7 +293,7 @@ async function runGatewaySuspensionPostRestartClient({ statePath, token, url }) emitPhase("post-restart", startedAt); } -export function responseError(method, response) { +function responseError(method, response) { const message = response.error?.message ?? "unknown"; return new Error(`${method} failed: ${message}`); } diff --git a/scripts/e2e/lib/mcp-code-mode-validation.ts b/scripts/e2e/lib/mcp-code-mode-validation.ts index 68e0746a8cd1..ec54fe283efc 100644 --- a/scripts/e2e/lib/mcp-code-mode-validation.ts +++ b/scripts/e2e/lib/mcp-code-mode-validation.ts @@ -9,7 +9,7 @@ function assert(condition: unknown, message: string): asserts condition { } } -export function outputText(response: unknown): string { +function outputText(response: unknown): string { const output = (response as { output?: Array<{ type?: unknown; content?: unknown }> }).output; if (!Array.isArray(output)) { return ""; diff --git a/scripts/e2e/lib/plugin-index-sqlite.d.mts b/scripts/e2e/lib/plugin-index-sqlite.d.mts index f12af51bca77..12b19ade92e0 100644 --- a/scripts/e2e/lib/plugin-index-sqlite.d.mts +++ b/scripts/e2e/lib/plugin-index-sqlite.d.mts @@ -9,8 +9,6 @@ export type PluginInstallRecord = Record & { spec?: string; }; -export function stateDir(): string; -export function configPath(): string; export function readPluginInstallIndex(options?: Record): unknown; export function readPluginInstallRecords(options?: { configPath?: string; diff --git a/scripts/e2e/lib/plugin-index-sqlite.mjs b/scripts/e2e/lib/plugin-index-sqlite.mjs index 066880da72d3..76be8acccea8 100644 --- a/scripts/e2e/lib/plugin-index-sqlite.mjs +++ b/scripts/e2e/lib/plugin-index-sqlite.mjs @@ -12,11 +12,11 @@ const JSON_ARTIFACT_MAX_BYTES = readPositiveIntEnv( 1024 * 1024, ); -export function stateDir() { +function stateDir() { return process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, ".openclaw"); } -export function configPath() { +function configPath() { return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json"); } diff --git a/scripts/e2e/parallels/common.ts b/scripts/e2e/parallels/common.ts index 4f0ddbb7f865..828f72cb9422 100644 --- a/scripts/e2e/parallels/common.ts +++ b/scripts/e2e/parallels/common.ts @@ -2,14 +2,21 @@ export * from "./filesystem.ts"; export * from "./env-limits.ts"; export * from "./host-command.ts"; -export * from "./host-server.ts"; +export { + resolveHostIp, + resolveHostPort, + startHostServer, + startNpmRegistryServer, +} from "./host-server.ts"; export * from "./lane-runner.ts"; export * from "./macos-users.ts"; -export * from "./package-artifact.ts"; -// host-server.ts and package-artifact.ts both export a module-private `testing` hook, which -// star re-exports silently drop as ambiguous. Re-export one explicitly so the barrel -// typechecks (TS2308); import either module's `testing` directly, never through this barrel. -export { testing } from "./host-server.ts"; +export { + extractPackageJsonFromTgz, + packOpenClaw, + packageBuildCommitFromTgz, + packageVersionFromTgz, + resolveOpenClawRegistryVersion, +} from "./package-artifact.ts"; export * from "./parallels-vm.ts"; export * from "./plugin-isolation.ts"; export * from "./provider-auth.ts"; diff --git a/scripts/e2e/parallels/filesystem.ts b/scripts/e2e/parallels/filesystem.ts index 40ffe7e5fbd5..6fd221a56c42 100644 --- a/scripts/e2e/parallels/filesystem.ts +++ b/scripts/e2e/parallels/filesystem.ts @@ -1,5 +1,5 @@ // Filesystem script supports OpenClaw repository automation. -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync } from "node:fs"; import { access, mkdir, open, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { repoRoot } from "./host-command.ts"; @@ -20,7 +20,7 @@ export async function readJson(filePath: string): Promise { return JSON.parse(await readFile(filePath, "utf8")) as T; } -export async function readTextFileTail( +async function readTextFileTail( filePath: string, maxBytes = DEFAULT_TEXT_FILE_TAIL_BYTES, ): Promise { @@ -86,7 +86,3 @@ export async function writeSummaryMarkdown(input: { ); return markdownPath; } - -export function writeExecutable(filePath: string, content: string): void { - writeFileSync(filePath, content, { encoding: "utf8", mode: 0o755 }); -} diff --git a/scripts/e2e/secret-provider-integrations.mjs b/scripts/e2e/secret-provider-integrations.mjs index 96f8d372aeb2..f51261659c29 100644 --- a/scripts/e2e/secret-provider-integrations.mjs +++ b/scripts/e2e/secret-provider-integrations.mjs @@ -2164,13 +2164,10 @@ async function main() { export { assertAllowedFailureCommandSucceeded, - clampSecretProofTimerTimeoutMs, collectBlockingProofResults, cleanupEnv, expectGatewayStartupFails, - gatewayCall, parseJsonOutput, - readPositiveTimerMs, runPtySecretsConfigurePreset, runWithProof, runCommand, diff --git a/scripts/e2e/telegram-user-crabbox-proof.ts b/scripts/e2e/telegram-user-crabbox-proof.ts index 03d9aed11eac..c7c8c2de5481 100644 --- a/scripts/e2e/telegram-user-crabbox-proof.ts +++ b/scripts/e2e/telegram-user-crabbox-proof.ts @@ -156,7 +156,7 @@ const COMMAND_STDOUT_MAX_CHARS = 1024 * 1024; const COMMAND_STDERR_TAIL_CHARS = 256 * 1024; const COMMAND_FAILURE_STDOUT_TAIL_CHARS = 64 * 1024; export const COMMAND_TIMEOUT_MS = 30 * 60 * 1000; -export const COMMAND_TIMEOUT_KILL_GRACE_MS = 5_000; +const COMMAND_TIMEOUT_KILL_GRACE_MS = 5_000; const COMMAND_PROCESS_TREE_EXIT_POLL_MS = 25; export const REMOTE_SETUP_COMMAND_TIMEOUT_MS = 90 * 60 * 1000; const REMOTE_ROOT = "/tmp/openclaw-telegram-user-crabbox"; diff --git a/scripts/generate-secretref-credential-matrix.ts b/scripts/generate-secretref-credential-matrix.ts deleted file mode 100644 index cc310bd610d4..000000000000 --- a/scripts/generate-secretref-credential-matrix.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Generate Secretref Credential Matrix script supports OpenClaw repository automation. -import fs from "node:fs"; -import path from "node:path"; -import { buildSecretRefCredentialMatrix } from "../src/secrets/credential-matrix.js"; - -const outputPath = path.join( - process.cwd(), - "docs", - "reference", - "secretref-user-supplied-credentials-matrix.json", -); - -const matrix = buildSecretRefCredentialMatrix(); -fs.writeFileSync(outputPath, `${JSON.stringify(matrix, null, 2)}\n`, "utf8"); -console.log(`Wrote ${outputPath}`); diff --git a/scripts/github/real-behavior-proof-policy.mjs b/scripts/github/real-behavior-proof-policy.mjs index 5b2c3172e900..d19eacce7ca2 100644 --- a/scripts/github/real-behavior-proof-policy.mjs +++ b/scripts/github/real-behavior-proof-policy.mjs @@ -7,7 +7,7 @@ export const PROOF_OVERRIDE_LABEL = "proof: override"; export const PROOF_SUFFICIENT_LABEL = "proof: sufficient"; export const NEEDS_PR_CONTEXT_LABEL = "triage: needs-pr-context"; const MAINTAINER_TEAM_SLUG = "maintainer"; -export const DEFAULT_GITHUB_API_TIMEOUT_MS = 30_000; +const DEFAULT_GITHUB_API_TIMEOUT_MS = 30_000; const GITHUB_API_RESPONSE_BODY_MAX_BYTES = 1024 * 1024; const CLAWSWEEPER_PROOF_VERDICT_STATUS = "clawsweeper_exact_head_pass"; diff --git a/scripts/lib/actions-artifact-archive.mjs b/scripts/lib/actions-artifact-archive.mjs index d1b569d365f7..8561175ac122 100644 --- a/scripts/lib/actions-artifact-archive.mjs +++ b/scripts/lib/actions-artifact-archive.mjs @@ -3,9 +3,9 @@ import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs"; import { basename } from "node:path"; import { inflateRawSync } from "node:zlib"; -export const ACTIONS_ARTIFACT_API_VERSION = "2026-03-10"; -export const DEFAULT_MAX_ACTIONS_ARTIFACT_BYTES = 256 * 1024 * 1024; -export const DEFAULT_MAX_ACTIONS_ARTIFACT_EXPANDED_BYTES = 512 * 1024 * 1024; +const ACTIONS_ARTIFACT_API_VERSION = "2026-03-10"; +const DEFAULT_MAX_ACTIONS_ARTIFACT_BYTES = 256 * 1024 * 1024; +const DEFAULT_MAX_ACTIONS_ARTIFACT_EXPANDED_BYTES = 512 * 1024 * 1024; const DEFAULT_MAX_JSON_BYTES = 2 * 1024 * 1024; const DEFAULT_TIMEOUT_MS = 60_000; diff --git a/scripts/lib/codex-app-server-protocol-source.ts b/scripts/lib/codex-app-server-protocol-source.ts index 1ef75aaf1f2b..b2a21d4bcaf8 100644 --- a/scripts/lib/codex-app-server-protocol-source.ts +++ b/scripts/lib/codex-app-server-protocol-source.ts @@ -20,7 +20,7 @@ export const selectedCodexAppServerJsonSchemas = [ "v2/TurnStartResponse.json", ] as const; -export type GeneratedCodexAppServerProtocolSource = { +type GeneratedCodexAppServerProtocolSource = { root: string; codexRepo: string; typescriptRoot: string; diff --git a/scripts/lib/cross-os-release-checks/config.ts b/scripts/lib/cross-os-release-checks/config.ts index 31e427ac46d4..0d3cc47f7d10 100644 --- a/scripts/lib/cross-os-release-checks/config.ts +++ b/scripts/lib/cross-os-release-checks/config.ts @@ -2,10 +2,10 @@ import type { ChildProcess } from "node:child_process"; import { basename, dirname, resolve, win32 as pathWin32 } from "node:path"; import { trimForSummary } from "./shared.ts"; -export type CrossOsSuite = "packaged-fresh" | "installer-fresh" | "packaged-upgrade" | "dev-update"; -export type CrossOsMode = "fresh" | "upgrade" | "both"; -export type CrossOsOsId = "ubuntu" | "windows" | "macos"; -export type ProviderId = "openai" | "anthropic" | "minimax"; +type CrossOsSuite = "packaged-fresh" | "installer-fresh" | "packaged-upgrade" | "dev-update"; +type CrossOsMode = "fresh" | "upgrade" | "both"; +type CrossOsOsId = "ubuntu" | "windows" | "macos"; +type ProviderId = "openai" | "anthropic" | "minimax"; export type ProviderConfig = { extensionId: string; secretEnv: string; @@ -670,7 +670,7 @@ export function updateTimeoutMs() { : 20 * 60 * 1000; } -export function updateStepTimeoutSeconds() { +function updateStepTimeoutSeconds() { return process.platform === "win32" ? CROSS_OS_WINDOWS_PACKAGED_UPGRADE_STEP_TIMEOUT_SECONDS : 1200; diff --git a/scripts/lib/cross-os-release-checks/install.ts b/scripts/lib/cross-os-release-checks/install.ts index 11850af4d15b..66e035c85c88 100644 --- a/scripts/lib/cross-os-release-checks/install.ts +++ b/scripts/lib/cross-os-release-checks/install.ts @@ -342,11 +342,11 @@ export function readProvidedCandidate(params: { }; } -export function readPackageJson(packageRoot: string): PackageJson { +function readPackageJson(packageRoot: string): PackageJson { return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")) as PackageJson; } -export function packageJsonHasScript(packageJson: PackageJson, scriptName: string) { +function packageJsonHasScript(packageJson: PackageJson, scriptName: string) { return typeof packageJson?.scripts?.[scriptName] === "string"; } @@ -740,7 +740,7 @@ export function readInstalledMetadata(prefixDir: string) { return readInstalledMetadataFromManifest(packageJson, packageRoot); } -export function readInstalledMetadataFromPackageRoot(packageRoot: string) { +function readInstalledMetadataFromPackageRoot(packageRoot: string) { const { packageJson } = readInstalledPackageManifestFromPackageRoot(packageRoot); return readInstalledMetadataFromManifest(packageJson, packageRoot); } @@ -822,7 +822,7 @@ export function resolveInstalledPackageRootFromCliPath( throw new Error(`Installed package manifest missing. Checked: ${checked.join(", ")}`); } -export function installedPackageRoot(prefixDir: string, platform = process.platform) { +function installedPackageRoot(prefixDir: string, platform = process.platform) { return platform === "win32" ? join(prefixDir, "node_modules", "openclaw") : join(prefixDir, "lib", "node_modules", "openclaw"); @@ -832,7 +832,7 @@ export function installedEntryPath(prefixDir: string) { return join(installedPackageRoot(prefixDir), "openclaw.mjs"); } -export function npmShimPath(prefixDir: string) { +function npmShimPath(prefixDir: string) { return process.platform === "win32" ? join(prefixDir, "npm.cmd") : join(prefixDir, "bin", "npm"); } @@ -840,7 +840,7 @@ export function binDirForPrefix(prefixDir: string) { return process.platform === "win32" ? prefixDir : join(prefixDir, "bin"); } -export function pnpmCommand() { +function pnpmCommand() { return process.platform === "win32" ? "pnpm.cmd" : "pnpm"; } @@ -848,7 +848,7 @@ export function npmCommand() { return process.platform === "win32" ? "npm.cmd" : "npm"; } -export function gitCommand() { +function gitCommand() { return process.platform === "win32" ? "git.exe" : "git"; } diff --git a/scripts/lib/docker-e2e-plan.d.mts b/scripts/lib/docker-e2e-plan.d.mts index 60ceabe57710..25da1b524667 100644 --- a/scripts/lib/docker-e2e-plan.d.mts +++ b/scripts/lib/docker-e2e-plan.d.mts @@ -59,7 +59,6 @@ export type DockerE2ePlan = { export const DEFAULT_LIVE_RETRIES: number; export const DEFAULT_E2E_BARE_IMAGE: string; export const DEFAULT_E2E_FUNCTIONAL_IMAGE: string; -export const DEFAULT_E2E_IMAGE: string; export const DEFAULT_PARALLELISM: number; export const DEFAULT_PROFILE: string; export const DEFAULT_RESOURCE_LIMITS: Record; diff --git a/scripts/lib/docker-e2e-plan.mjs b/scripts/lib/docker-e2e-plan.mjs index 2694cf698e5e..676e1a12773e 100644 --- a/scripts/lib/docker-e2e-plan.mjs +++ b/scripts/lib/docker-e2e-plan.mjs @@ -20,7 +20,6 @@ export { normalizeReleaseProfile }; export const DEFAULT_E2E_BARE_IMAGE = "openclaw-docker-e2e-bare:local"; export const DEFAULT_E2E_FUNCTIONAL_IMAGE = "openclaw-docker-e2e-functional:local"; -export const DEFAULT_E2E_IMAGE = DEFAULT_E2E_FUNCTIONAL_IMAGE; export const DEFAULT_PARALLELISM = 10; export const DEFAULT_PROFILE = "all"; export const DEFAULT_RESOURCE_LIMITS = { diff --git a/scripts/lib/docker-plugin-selection.mjs b/scripts/lib/docker-plugin-selection.mjs index 4a419c2dda63..04f95930e3e3 100644 --- a/scripts/lib/docker-plugin-selection.mjs +++ b/scripts/lib/docker-plugin-selection.mjs @@ -32,7 +32,7 @@ function collectPluginIdentities(extensionsRoot) { } /** Resolve public Docker selections to the source directories used by build and prune steps. */ -export function resolveDockerPluginSelection(params) { +function resolveDockerPluginSelection(params) { const selection = typeof params.selection === "string" ? params.selection : ""; const selectedIds = new Set( selection diff --git a/scripts/lib/gateway-bench-child.ts b/scripts/lib/gateway-bench-child.ts index 263c7fbccf40..c531fa86f6ed 100644 --- a/scripts/lib/gateway-bench-child.ts +++ b/scripts/lib/gateway-bench-child.ts @@ -9,7 +9,7 @@ const TEARDOWN_GRACE_MS = 2_000; const TEARDOWN_KILL_GRACE_MS = 1_000; const EXIT_POLL_MS = 10; -export type ChildExit = { +type ChildExit = { exitCode: number | null; signal: string | null; }; diff --git a/scripts/lib/gateway-ws-client.ts b/scripts/lib/gateway-ws-client.ts index bc2cf8e9869f..b573ef45db0f 100644 --- a/scripts/lib/gateway-ws-client.ts +++ b/scripts/lib/gateway-ws-client.ts @@ -20,8 +20,8 @@ type GatewayResFrame = { payload?: unknown; error?: unknown; }; -export type GatewayEventFrame = { type: "event"; event: string; seq?: number; payload?: unknown }; -export type GatewayFrame = +type GatewayEventFrame = { type: "event"; event: string; seq?: number; payload?: unknown }; +type GatewayFrame = | GatewayReqFrame | GatewayResFrame | GatewayEventFrame diff --git a/scripts/lib/openclaw-test-state.mjs b/scripts/lib/openclaw-test-state.mjs index 92f09cccfb3e..4a58ceaf670e 100644 --- a/scripts/lib/openclaw-test-state.mjs +++ b/scripts/lib/openclaw-test-state.mjs @@ -323,7 +323,7 @@ function buildCreatePlan(options = {}) { } /** Create an isolated OpenClaw test state directory and optional scenario config. */ -export async function createState(options = {}) { +async function createState(options = {}) { const label = normalizeLabel(options.label); const root = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-${label}-`)); const plan = buildCreatePlan({ ...options, root }); diff --git a/scripts/lib/plain-gh.d.mts b/scripts/lib/plain-gh.d.mts index 0437faa7302f..5753b212e9a8 100644 --- a/scripts/lib/plain-gh.d.mts +++ b/scripts/lib/plain-gh.d.mts @@ -2,10 +2,6 @@ import type { ExecFileSyncOptions, ExecFileSyncOptionsWithBufferEncoding, ExecFileSyncOptionsWithStringEncoding, - SpawnSyncOptions, - SpawnSyncOptionsWithBufferEncoding, - SpawnSyncOptionsWithStringEncoding, - SpawnSyncReturns, } from "node:child_process"; export function plainGhEnv(env?: NodeJS.ProcessEnv): { @@ -36,16 +32,4 @@ export function execGhApiRead( endpoint: string, options?: ExecFileSyncOptions, ): string | Uint8Array; -export function spawnPlainGh( - args: readonly string[], - options: SpawnSyncOptionsWithStringEncoding, -): SpawnSyncReturns; -export function spawnPlainGh( - args: readonly string[], - options?: SpawnSyncOptionsWithBufferEncoding, -): SpawnSyncReturns>; -export function spawnPlainGh( - args: readonly string[], - options?: SpawnSyncOptions, -): SpawnSyncReturns>; export const PLAIN_GH_SYSTEM_CANDIDATES: string[]; diff --git a/scripts/lib/plugin-gateway-gauntlet.d.mts b/scripts/lib/plugin-gateway-gauntlet.d.mts index e7bcef8af633..dcce25e49dee 100644 --- a/scripts/lib/plugin-gateway-gauntlet.d.mts +++ b/scripts/lib/plugin-gateway-gauntlet.d.mts @@ -126,7 +126,6 @@ export function readQaSuiteSummary(summaryPath: unknown): diagnosticDetail: null; summary: unknown; }; -export function schemaHasRequiredFields(schema: unknown, seen?: Set): boolean; export function selectPluginEntries( entries: T[], options?: { ids?: string[]; shardIndex?: number; shardTotal?: number }, diff --git a/scripts/lib/plugin-gateway-gauntlet.mjs b/scripts/lib/plugin-gateway-gauntlet.mjs index b8d63693fbda..7600ed1926a7 100644 --- a/scripts/lib/plugin-gateway-gauntlet.mjs +++ b/scripts/lib/plugin-gateway-gauntlet.mjs @@ -633,6 +633,5 @@ export { detectCommandDiagnosticFailure, discoverBundledPluginManifests, readQaSuiteSummary, - schemaHasRequiredFields, selectPluginEntries, }; diff --git a/scripts/lib/plugin-npm-release.ts b/scripts/lib/plugin-npm-release.ts index 053b9affe310..e3e62624868f 100644 --- a/scripts/lib/plugin-npm-release.ts +++ b/scripts/lib/plugin-npm-release.ts @@ -12,7 +12,7 @@ import { resolveNpmPublishPlan, } from "./npm-publish-plan.mjs"; -export type PluginPackageJson = { +type PluginPackageJson = { name?: string; version?: string; type?: string; @@ -64,11 +64,11 @@ export type PublishablePluginPackage = { requiredLatestDependencies?: RequiredLatestDependency[]; }; -export type PluginReleasePlanItem = PublishablePluginPackage & { +type PluginReleasePlanItem = PublishablePluginPackage & { alreadyPublished: boolean; }; -export type PluginReleasePlan = { +type PluginReleasePlan = { all: PluginReleasePlanItem[]; candidates: PluginReleasePlanItem[]; skippedPublished: PluginReleasePlanItem[]; diff --git a/scripts/lib/sqlite-reliability-contract.ts b/scripts/lib/sqlite-reliability-contract.ts index ec224330c457..26c5af8ba6fb 100644 --- a/scripts/lib/sqlite-reliability-contract.ts +++ b/scripts/lib/sqlite-reliability-contract.ts @@ -1,6 +1,6 @@ export type ProfileId = "smoke" | "default" | "large"; -export type ProfileConfig = { +type ProfileConfig = { iterations: number; maxWalBytes: number; payloadBytes: number; @@ -18,7 +18,7 @@ export type CliOptions = { stateDir: string | null; }; -export type ReliabilityStateProof = { +type ReliabilityStateProof = { batches: number; rows: number; sha256: string; diff --git a/scripts/lib/sqlite-reliability-writer.ts b/scripts/lib/sqlite-reliability-writer.ts index a2ab9b840596..33ce979eaa0d 100644 --- a/scripts/lib/sqlite-reliability-writer.ts +++ b/scripts/lib/sqlite-reliability-writer.ts @@ -12,7 +12,7 @@ type WriterReadyMessage = { kind: "ready"; }; -export type WriterPartialMessage = { +type WriterPartialMessage = { batch: number; kind: "partial"; rows: number; @@ -23,7 +23,7 @@ type WriterReleasedMessage = { kind: "released"; }; -export type WriterResultMessage = { +type WriterResultMessage = { batchesCommitted: number; kind: "result"; rowsCommitted: number; diff --git a/scripts/lib/sqlite-session-schema-baseline.ts b/scripts/lib/sqlite-session-schema-baseline.ts index b1fbed87151c..c076f83eb7c6 100644 --- a/scripts/lib/sqlite-session-schema-baseline.ts +++ b/scripts/lib/sqlite-session-schema-baseline.ts @@ -5,13 +5,13 @@ import path from "node:path"; import { expectDefined } from "../../packages/normalization-core/src/expect.js"; /** Rendered baseline artifact for the sessions/transcripts SQLite schema. */ -export type SqliteSessionSchemaBaselineRender = { +type SqliteSessionSchemaBaselineRender = { /** Normalized SQL for the session, conversation, and transcript schema objects. */ sql: string; }; /** Result returned after writing or checking SQLite schema baseline artifacts. */ -export type SqliteSessionSchemaBaselineWriteResult = { +type SqliteSessionSchemaBaselineWriteResult = { /** True when generated artifact content differs from disk. */ changed: boolean; /** True when changed artifacts were actually written. */ diff --git a/scripts/mantis/build-telegram-desktop-proof-evidence.mjs b/scripts/mantis/build-telegram-desktop-proof-evidence.mjs index 7c7373bfbc17..92fa2eb623eb 100644 --- a/scripts/mantis/build-telegram-desktop-proof-evidence.mjs +++ b/scripts/mantis/build-telegram-desktop-proof-evidence.mjs @@ -163,7 +163,7 @@ function laneArtifactEntries() { /** * Builds the manifest for paired baseline/candidate Telegram Desktop proof artifacts. */ -export function buildTelegramDesktopProofManifest({ +function buildTelegramDesktopProofManifest({ baseline, baselineRef, baselineSha, diff --git a/scripts/mcp-code-mode-gateway-e2e.ts b/scripts/mcp-code-mode-gateway-e2e.ts index a751f3f70059..63fd7a3de8db 100644 --- a/scripts/mcp-code-mode-gateway-e2e.ts +++ b/scripts/mcp-code-mode-gateway-e2e.ts @@ -181,7 +181,7 @@ async function writeConfig(params: { await fs.writeFile(params.configPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8"); } -export async function main() { +async function main() { const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-mcp-code-mode-")); const keep = process.env.OPENCLAW_MCP_CODE_MODE_GATEWAY_E2E_KEEP === "1"; const previousEnv = { diff --git a/scripts/measure-rpc-rtt.mjs b/scripts/measure-rpc-rtt.mjs index f1fe4698e8ff..a84a0b71fee5 100644 --- a/scripts/measure-rpc-rtt.mjs +++ b/scripts/measure-rpc-rtt.mjs @@ -17,7 +17,7 @@ const DEFAULT_METHODS = ["health", "config.get"]; const DEFAULT_ITERATIONS = 10; const SINGLE_VALUE_FLAGS = new Set(["--iterations", "--methods", "--output-dir", "--repo-root"]); /** Maximum time to wait for a spawned gateway to become reachable. */ -export const READY_TIMEOUT_MS = 120_000; +const READY_TIMEOUT_MS = 120_000; /** Per-probe timeout used while polling gateway readiness endpoints. */ const READY_PROBE_TIMEOUT_MS = 1_000; const READY_PROBE_RESPONSE_BODY_MAX_BYTES = 64 * 1024; diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index f81bf6cab3b2..4f0dc74e8293 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -47,7 +47,7 @@ function isMainModule() { return resolve(invokedPath) === SCRIPT_PATH; } -export async function main(argv: string[]) { +async function main(argv: string[]) { const args = parseArgs(argv); if (args["resolve-matrix"] === "true") { diff --git a/scripts/openclaw-npm-resume-run.mjs b/scripts/openclaw-npm-resume-run.mjs index 2d1315939692..f5c87ca905ef 100644 --- a/scripts/openclaw-npm-resume-run.mjs +++ b/scripts/openclaw-npm-resume-run.mjs @@ -155,7 +155,7 @@ function parseArgs(argv) { return options; } -export function main(argv = process.argv.slice(2)) { +function main(argv = process.argv.slice(2)) { const result = resolveOpenClawNpmResumeRun(parseArgs(argv)); process.stdout.write(`${JSON.stringify(result)}\n`); } diff --git a/scripts/perf/summarize-cpuprofile.mjs b/scripts/perf/summarize-cpuprofile.mjs index 04bb69414712..e9a78a83f84b 100644 --- a/scripts/perf/summarize-cpuprofile.mjs +++ b/scripts/perf/summarize-cpuprofile.mjs @@ -8,7 +8,7 @@ import { parsePositiveInt } from "../lib/numeric-options.mjs"; const DEFAULT_LIMIT = 30; -export function usage() { +function usage() { return "Usage: scripts/perf/summarize-cpuprofile.mjs [--limit N] "; } @@ -120,7 +120,7 @@ function validateProfile(profile, file) { } } -export function summarizeProfile(file, limit) { +function summarizeProfile(file, limit) { const profile = JSON.parse(fs.readFileSync(file, "utf8")); validateProfile(profile, file); const nodes = new Map(profile.nodes.map((node) => [node.id, node])); diff --git a/scripts/qa/ux-matrix-evidence-producer.ts b/scripts/qa/ux-matrix-evidence-producer.ts index 6cc78f649b99..5241464d9f45 100644 --- a/scripts/qa/ux-matrix-evidence-producer.ts +++ b/scripts/qa/ux-matrix-evidence-producer.ts @@ -40,7 +40,7 @@ type MatrixCell = { type ChromiumLauncher = Awaited["chromium"]; type ChromiumBrowser = Awaited>; -export type ProducerOptions = { +type ProducerOptions = { artifactBase: string; repoRoot: string; skipVisualProof: boolean; diff --git a/scripts/render-github-release-notes.mjs b/scripts/render-github-release-notes.mjs index 71736f035063..4e4b5e2eefde 100644 --- a/scripts/render-github-release-notes.mjs +++ b/scripts/render-github-release-notes.mjs @@ -40,14 +40,14 @@ function validateTag(tag) { } } -export function githubReleaseBodySize(body) { +function githubReleaseBodySize(body) { return { characters: [...body].length, bytes: Buffer.byteLength(body, "utf8"), }; } -export function fitsGithubReleaseBody(body) { +function fitsGithubReleaseBody(body) { const size = githubReleaseBodySize(body); return ( size.characters <= GITHUB_RELEASE_BODY_MAX_CHARACTERS && @@ -185,7 +185,7 @@ export function parseShippedBaselineExclusions(section) { return baselines; } -export function tagPinnedContributionRecordUrl(repository, tag) { +function tagPinnedContributionRecordUrl(repository, tag) { validateRepository(repository); validateTag(tag); return `https://github.com/${repository}/blob/${tag}/CHANGELOG.md#complete-contribution-record`; diff --git a/scripts/report-test-temp-creations.mjs b/scripts/report-test-temp-creations.mjs index b4527bf42f5a..c290d4255096 100644 --- a/scripts/report-test-temp-creations.mjs +++ b/scripts/report-test-temp-creations.mjs @@ -435,7 +435,7 @@ export function collectTempCreationFindingsFromDiff(diffText, options = {}) { return findings; } -export async function main(argv, io) { +async function main(argv, io) { const args = parseArgs(argv ?? process.argv.slice(2)); const stdout = io?.stdout ?? process.stdout; const stderr = io?.stderr ?? process.stderr; diff --git a/scripts/sqlite-vec-smoke.mjs b/scripts/sqlite-vec-smoke.mjs deleted file mode 100644 index ff6175cf834e..000000000000 --- a/scripts/sqlite-vec-smoke.mjs +++ /dev/null @@ -1,41 +0,0 @@ -// Verifies sqlite-vec can load and execute a simple vector query in Node's -// built-in SQLite runtime. -import { DatabaseSync } from "node:sqlite"; -import { load, getLoadablePath } from "sqlite-vec"; -import { formatErrorMessage } from "./lib/error-format.mjs"; - -function vec(values) { - return Buffer.from(new Float32Array(values).buffer); -} - -const db = new DatabaseSync(":memory:", { allowExtension: true }); - -try { - load(db); -} catch (err) { - const message = formatErrorMessage(err); - console.error("sqlite-vec load failed:"); - console.error(message); - console.error("expected extension path:", getLoadablePath()); - process.exit(1); -} - -db.exec(` - CREATE VIRTUAL TABLE v USING vec0( - id TEXT PRIMARY KEY, - embedding FLOAT[4] - ); -`); - -const insert = db.prepare("INSERT INTO v (id, embedding) VALUES (?, ?)"); -insert.run("a", vec([1, 0, 0, 0])); -insert.run("b", vec([0, 1, 0, 0])); -insert.run("c", vec([0.2, 0.2, 0, 0])); - -const query = vec([1, 0, 0, 0]); -const rows = db - .prepare("SELECT id, vec_distance_cosine(embedding, ?) AS dist FROM v ORDER BY dist ASC") - .all(query); - -console.log("sqlite-vec ok"); -console.log(rows); diff --git a/scripts/sync-openclaw-label-colors.mjs b/scripts/sync-openclaw-label-colors.mjs deleted file mode 100644 index 1f3499f8c5e3..000000000000 --- a/scripts/sync-openclaw-label-colors.mjs +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env node -// Synchronizes GitHub label colors to the OpenClaw taxonomy policy. -import { execFileSync } from "node:child_process"; - -const REPO = "openclaw/openclaw"; -const APPLY = process.argv.includes("--apply"); - -const COLORS = { - saturatedRed: "B60205", - saturatedBugRed: "D73A4A", - saturatedOrangeRed: "D93F0B", - saturatedAmber: "FBCA04", - softerAmber: "F9D65C", - paleYellow: "F7E7A1", - saturatedGreen: "0E8A16", - mutedGreen: "8C959F", - paleGreen: "D6E3DA", - proofGreen: "2DA44E", - mutedProofGreen: "1A7F37", - overrideGreen: "2DA44E", - saturatedBlue: "0F2CCE", - paleBlue: "0A3069", - channelBlue: "0969DA", - dedupeBlue: "57606A", - triageBlue: "0969DA", - saturatedPurple: "7057FF", - mutedPurple: "57606A", - taxonomyGray: "6E7781", - taxonomySteel: "57606A", - appPurple: "6E7781", - neutralGray: "E5E7EB", - duplicateGray: "D1D5DB", - darkGray: "8C8C8C", - mutedRose: "8C959F", - mutedRed: "E99695", - black: "000000", - white: "FFFFFF", - clawsweeperOrange: "F97316", - helpGreen: "008672", - maintainerYellow: "FFD700", -}; - -const EXACT_COLORS = new Map( - Object.entries({ - P0: COLORS.saturatedRed, - P1: COLORS.saturatedOrangeRed, - P2: COLORS.saturatedAmber, - P3: COLORS.mutedGreen, - "rating: 🦀 challenger crab": "1F883D", - "rating: 🦞 diamond lobster": "0969DA", - "rating: 🐚 platinum hermit": "0F766E", - "rating: 🦐 gold shrimp": "B7791F", - "rating: 🦪 silver shellfish": "7A828E", - "rating: 🧂 unranked krab": "8C2F39", - "rating: 🌊 off-meta tidepool": "6E7781", - "impact:data-loss": COLORS.saturatedRed, - "impact:security": COLORS.saturatedRed, - "impact:crash-loop": COLORS.saturatedOrangeRed, - "impact:message-loss": COLORS.saturatedOrangeRed, - "impact:auth-provider": COLORS.softerAmber, - "impact:session-state": COLORS.softerAmber, - bug: COLORS.saturatedBugRed, - "bug:crash": COLORS.saturatedRed, - "bug:behavior": COLORS.saturatedBugRed, - regression: COLORS.saturatedOrangeRed, - security: COLORS.saturatedRed, - "beta-blocker": COLORS.saturatedOrangeRed, - "proof: supplied": COLORS.proofGreen, - "proof: sufficient": COLORS.mutedProofGreen, - "proof: override": COLORS.overrideGreen, - "triage:blocked": COLORS.saturatedAmber, - "triage:bug": COLORS.saturatedBugRed, - "triage:done": COLORS.mutedGreen, - "triage:needs-review": COLORS.paleBlue, - "triage:started": COLORS.mutedPurple, - agents: COLORS.taxonomySteel, - docs: COLORS.paleBlue, - cli: COLORS.paleBlue, - commands: COLORS.paleBlue, - scripts: COLORS.mutedPurple, - gateway: COLORS.mutedPurple, - codex: COLORS.taxonomySteel, - docker: COLORS.paleGreen, - tui: COLORS.paleGreen, - "extensions: NEW": COLORS.channelBlue, - clawsweeper: COLORS.clawsweeperOrange, - "clawsweeper:human-review": COLORS.saturatedRed, - "clawsweeper:needs-security-review": COLORS.saturatedRed, - "clawsweeper:needs-live-repro": COLORS.saturatedAmber, - "clawsweeper:needs-maintainer-review": COLORS.saturatedAmber, - "clawsweeper:needs-product-decision": COLORS.saturatedAmber, - "clawsweeper:automerge": COLORS.saturatedGreen, - "clawsweeper:queueable-fix": COLORS.saturatedGreen, - "clawsweeper:fix-shape-clear": COLORS.mutedProofGreen, - "clawsweeper:autofix": COLORS.paleBlue, - "clawsweeper:commit-finding": COLORS.paleBlue, - "clawsweeper:autogenerated": COLORS.mutedPurple, - "clawsweeper:linked-pr-open": COLORS.mutedPurple, - "clawsweeper:merge-ready": COLORS.mutedPurple, - "clawsweeper:current-main-repro": COLORS.paleBlue, - "clawsweeper:source-repro": COLORS.paleBlue, - "clawsweeper:not-repro-on-main": COLORS.proofGreen, - "clawsweeper:no-new-fix-pr": COLORS.neutralGray, - "clawsweeper:needs-info": COLORS.appPurple, - "close:spam": COLORS.black, - "close:duplicate": COLORS.mutedRed, - "close:already-fixed": COLORS.paleBlue, - "close:cannot-repro": COLORS.paleYellow, - "close:invalid": COLORS.neutralGray, - "close:not-planned": COLORS.darkGray, - "close:superseded": COLORS.mutedPurple, - duplicate: COLORS.duplicateGray, - invalid: COLORS.paleYellow, - wontfix: COLORS.white, - "r: spam": COLORS.saturatedRed, - "r: moltbook": COLORS.saturatedRed, - "r: too-many-prs": COLORS.saturatedOrangeRed, - "r: no-ci-pr": COLORS.saturatedOrangeRed, - "r: bluebubbles": COLORS.saturatedOrangeRed, - "r: testflight": COLORS.saturatedOrangeRed, - "r: false-positive": COLORS.saturatedOrangeRed, - "r: skill": COLORS.mutedPurple, - "r: third-party-extension": COLORS.mutedPurple, - "r: support": COLORS.mutedGreen, - "r: too-many-prs-override": COLORS.overrideGreen, - maintainer: COLORS.maintainerYellow, - "trusted-contributor": COLORS.neutralGray, - "help wanted": COLORS.helpGreen, - "good first issue": COLORS.saturatedPurple, - "In Progress": COLORS.saturatedBlue, - "no-stale": COLORS.mutedGreen, - stale: COLORS.paleYellow, - "trigger-response": COLORS.saturatedAmber, - "bad-barnacle": COLORS.mutedRed, - clownfish: COLORS.neutralGray, - "mantis: telegram-visible-proof": COLORS.mutedPurple, - "dedupe:parent": COLORS.dedupeBlue, - "dedupe:child": COLORS.paleBlue, - dependencies: COLORS.neutralGray, - "dependencies-changed": COLORS.paleYellow, - "security-sensitive-changed": COLORS.paleYellow, - github_actions: COLORS.neutralGray, - go: COLORS.neutralGray, - java: COLORS.neutralGray, - swift_package_manager: COLORS.neutralGray, - "cannot-reproduce": COLORS.dedupeBlue, - question: COLORS.appPurple, - enhancement: COLORS.channelBlue, - "pi-issue": COLORS.saturatedOrangeRed, - aardvark: COLORS.neutralGray, - "cohort-4": COLORS.mutedPurple, - dirty: COLORS.saturatedRed, - }), -); - -const FAMILY_RULES = [ - { - family: "channel", - match: (name) => name.startsWith("channel: "), - color: COLORS.channelBlue, - reason: "routine channel taxonomy stays visually quiet", - }, - { - family: "app", - match: (name) => name.startsWith("app: "), - color: COLORS.appPurple, - reason: "app platform taxonomy stays muted", - }, - { - family: "extension", - match: (name) => name.startsWith("extensions: "), - color: COLORS.taxonomyGray, - reason: "plugin implementation taxonomy should not compete with priority", - }, - { - family: "plugin", - match: (name) => name.startsWith("plugin: "), - color: COLORS.taxonomyGray, - reason: "plugin taxonomy stays neutral unless it becomes an action gate", - }, - { - family: "size", - match: (name) => name.startsWith("size: "), - color: COLORS.mutedRose, - reason: "size text carries the value, so one muted family color is enough", - }, - { - family: "triage-candidate", - match: (name) => name.startsWith("triage:"), - color: COLORS.triageBlue, - reason: "routine triage candidates stay softer than blockers and priorities", - }, -]; - -function wantedPolicy(name) { - if (EXACT_COLORS.has(name)) { - return { color: EXACT_COLORS.get(name), family: exactFamily(name), source: "exact" }; - } - const rule = FAMILY_RULES.find((candidate) => candidate.match(name)); - if (rule) { - return { color: rule.color, family: rule.family, source: "family" }; - } - return null; -} - -function exactFamily(name) { - if (/^P[0-3]$/.test(name)) { - return "priority"; - } - if (name.startsWith("rating:")) { - return "rating"; - } - if (name.startsWith("impact:")) { - return "impact"; - } - if (name.startsWith("clawsweeper")) { - return "clawsweeper"; - } - if (name.startsWith("close:") || name.startsWith("r:")) { - return "resolution"; - } - if (name.startsWith("proof:")) { - return "proof"; - } - if (name.startsWith("triage:")) { - return "triage-status"; - } - if (name.startsWith("bug") || name === "security" || name === "regression") { - return "risk"; - } - return "specific"; -} - -function ghJson(args) { - return JSON.parse(execFileSync("gh", args, { encoding: "utf8" })); -} - -function gh(args) { - execFileSync("gh", args, { encoding: "utf8", stdio: APPLY ? "inherit" : "pipe" }); -} - -function fetchLabels() { - const labels = []; - let cursor = null; - for (;;) { - const query = `query($cursor:String) { - repository(owner:"openclaw", name:"openclaw") { - labels(first:100, after:$cursor, orderBy:{field:NAME,direction:ASC}) { - nodes { name color description } - pageInfo { hasNextPage endCursor } - } - } - }`; - const response = ghJson([ - "api", - "graphql", - "-f", - `query=${query}`, - "-f", - `cursor=${cursor ?? ""}`, - ]); - const page = response.data.repository.labels; - labels.push(...page.nodes); - if (!page.pageInfo.hasNextPage) { - return labels; - } - cursor = page.pageInfo.endCursor; - } -} - -const changes = fetchLabels() - .map((label) => ({ - name: label.name, - current: label.color.toUpperCase(), - policy: wantedPolicy(label.name), - })) - .filter((label) => label.policy && label.current !== label.policy.color) - .toSorted((a, b) => a.name.localeCompare(b.name)); - -if (changes.length === 0) { - console.log("No label color changes needed."); - process.exit(0); -} - -const familyCounts = new Map(); -for (const change of changes) { - familyCounts.set(change.policy.family, (familyCounts.get(change.policy.family) ?? 0) + 1); - console.log( - `${APPLY ? "update" : "would update"} [${change.policy.family}] ${change.name}: ${change.current} -> ${change.policy.color}`, - ); - if (APPLY) { - gh(["label", "edit", change.name, "--repo", REPO, "--color", change.policy.color]); - } -} - -console.log("Family summary:"); -for (const [family, count] of [...familyCounts.entries()].toSorted(([a], [b]) => - a.localeCompare(b), -)) { - console.log(`- ${family}: ${count}`); -} -console.log(`${APPLY ? "Updated" : "Would update"} ${changes.length} labels.`); diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index 0073d3e35708..8a14643f907c 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -9,7 +9,6 @@ import { fileURLToPath } from "node:url"; import { DEFAULT_E2E_BARE_IMAGE, DEFAULT_E2E_FUNCTIONAL_IMAGE, - DEFAULT_E2E_IMAGE, DEFAULT_LIVE_RETRIES, DEFAULT_PARALLELISM, DEFAULT_PROFILE, @@ -340,7 +339,7 @@ function buildLaneRerunCommand(name, baseEnv) { ["OPENCLAW_DOCKER_ALL_BUILD", build], ["OPENCLAW_DOCKER_ALL_PREFLIGHT", "0"], ["OPENCLAW_SKIP_DOCKER_BUILD", "1"], - ["OPENCLAW_DOCKER_E2E_IMAGE", image || DEFAULT_E2E_IMAGE], + ["OPENCLAW_DOCKER_E2E_IMAGE", image || DEFAULT_E2E_FUNCTIONAL_IMAGE], ["OPENCLAW_DOCKER_E2E_BARE_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_BARE_IMAGE], ["OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE], ["OPENCLAW_CURRENT_PACKAGE_TGZ", baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ], diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 52480f95392d..d0d29486db82 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -4422,7 +4422,7 @@ export function buildFullSuiteVitestRunPlans(args, cwd = process.cwd()) { }); } -export function shouldUseLocalFullSuiteParallelByDefault(env = process.env) { +function shouldUseLocalFullSuiteParallelByDefault(env = process.env) { if (hasConservativeVitestWorkerBudget(env)) { return false; } @@ -4431,7 +4431,7 @@ export function shouldUseLocalFullSuiteParallelByDefault(env = process.env) { ); } -export function shouldExpandLocalFullSuiteShardsByDefault(env = process.env) { +function shouldExpandLocalFullSuiteShardsByDefault(env = process.env) { return env.CI !== "true" && env.GITHUB_ACTIONS !== "true"; } diff --git a/scripts/testbox-lease-freshness.mjs b/scripts/testbox-lease-freshness.mjs index 84ce5915f9f9..534babe17993 100644 --- a/scripts/testbox-lease-freshness.mjs +++ b/scripts/testbox-lease-freshness.mjs @@ -66,7 +66,7 @@ function digestInputs(repoRoot, inputs) { return hash.digest("hex"); } -export function buildTestboxLeaseFingerprint(repoRoot, args) { +function buildTestboxLeaseFingerprint(repoRoot, args) { let baseSha; try { baseSha = git(repoRoot, ["merge-base", "HEAD", "refs/remotes/origin/main"]); diff --git a/scripts/verify-pr-hosted-gates.mjs b/scripts/verify-pr-hosted-gates.mjs index 2bf00df29850..e3f4859aaad5 100644 --- a/scripts/verify-pr-hosted-gates.mjs +++ b/scripts/verify-pr-hosted-gates.mjs @@ -476,7 +476,7 @@ function loadPullRequestCommitShas(repo, { baseSha, headSha }) { return shas; } -export function main(argv = process.argv.slice(2)) { +function main(argv = process.argv.slice(2)) { const args = parseArgs(argv); const pullRequest = JSON.parse( execGhApiRead(`repos/${args.repo}/pulls/${args.pr}`, { diff --git a/scripts/verify-template-cache-bound.mjs b/scripts/verify-template-cache-bound.mjs deleted file mode 100644 index b5679377edee..000000000000 --- a/scripts/verify-template-cache-bound.mjs +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Real-process verification: bounded template file cache. - * - * Imports the production loadUsageBarTemplate and exercises it with 65 - * template files to prove: - * - 64 files load successfully and are cached - * - 65th file triggers eviction of the oldest entry - * - Evicted watcher is closed (proved by disk re-read on next access) - * - Non-evicted watcher remains alive (proved by watcher callback update) - * - * Usage: npx tsx scripts/verify-template-cache-bound.mjs - */ - -import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const startTime = new Date().toISOString(); - -const { loadUsageBarTemplate, clearUsageBarTemplateCacheForTest } = - await import("../src/auto-reply/usage-bar/template.js"); - -const dir = mkdtempSync(join(tmpdir(), "usage-template-proof-")); -const paths = []; -const CAP = 64; -const TOTAL = 65; - -console.log("=".repeat(72)); -console.log("OpenClaw Template Cache Bound — Real-Process Verification"); -console.log("=".repeat(72)); -console.log(`PID: ${process.pid}`); -console.log(`Node: ${process.version}`); -console.log(`Platform: ${process.platform} ${process.arch}`); -console.log(`Started: ${startTime}`); -console.log(`Temp dir: ${dir}`); -console.log(`Cache cap: ${CAP}`); -console.log(`Files: ${TOTAL}`); -console.log(); - -for (let i = 0; i < TOTAL; i++) { - const p = join(dir, `tpl-${String(i).padStart(3, "0")}.json`); - writeFileSync(p, JSON.stringify({ segments: [{ text: `v1-${i}` }] })); - paths.push(p); -} - -// Phase 1: Fill cache -console.log("── Phase 1: Fill cache (files 0–63) ──"); -const t1 = Date.now(); -for (let i = 0; i < CAP; i++) { - const tpl = loadUsageBarTemplate(paths[i]); - if (!tpl || tpl.segments?.[0]?.text !== `v1-${i}`) { - console.log(` FAIL at file ${i}`); - process.exit(1); - } -} -console.log(` Duration: ${Date.now() - t1}ms`); -console.log(` Result: ${CAP} files loaded and cached`); -console.log(); - -// Phase 2: 65th file triggers eviction -console.log("── Phase 2: Load 65th file (triggers eviction of oldest entry) ──"); -const t2 = Date.now(); -const tpl64 = loadUsageBarTemplate(paths[64]); -console.log(` Duration: ${Date.now() - t2}ms`); -console.log(` File 64: ${JSON.stringify(tpl64)}`); -console.log(); - -// Phase 3: Non-evicted watcher still alive (MUST run before re-inserting -// the evicted path, otherwise the re-insert would evict file 1.) -console.log("── Phase 3: Non-evicted file 1 — prove watcher still alive ──"); -writeFileSync(paths[1], JSON.stringify({ segments: [{ text: "CHANGED-VIA-WATCHER" }] })); -// fs.watch uses a polling fallback on Linux; give the watcher time to fire. -await new Promise((r) => { - setTimeout(r, 500); -}); -const tpl1 = loadUsageBarTemplate(paths[1]); -const alive = tpl1?.segments?.[0]?.text === "CHANGED-VIA-WATCHER"; -console.log(` File 1 reloaded: "${tpl1?.segments?.[0]?.text}"`); -console.log( - ` Result: ${alive ? "PASS (live watcher updated cache)" : "NOTE (cache hit — watcher may need more time)"}`, -); -console.log(); - -// Phase 4: Cache integrity (MUST run before reloading the evicted path.) -console.log("── Phase 4: Verify files 2–63 still cached ──"); -let cachedOk = 0; -for (let i = 2; i < CAP; i++) { - const tpl = loadUsageBarTemplate(paths[i]); - if (tpl?.segments?.[0]?.text === `v1-${i}`) { - cachedOk++; - } -} -console.log(` Cached: ${cachedOk}/${CAP - 2}`); -console.log(` Result: ${cachedOk === CAP - 2 ? "PASS" : "FAIL"}`); -console.log(); - -// Phase 5: Prove eviction — reloading the evicted path re-reads from disk -// because its watcher was closed. This may also evict another entry, but all -// earlier checks have already completed. -console.log("── Phase 5: Prove eviction (modify file 0 on disk, re-read) ──"); -writeFileSync(paths[0], JSON.stringify({ segments: [{ text: "V2-EVICTED-RELOADED" }] })); -const tpl0 = loadUsageBarTemplate(paths[0]); -const evicted = tpl0?.segments?.[0]?.text === "V2-EVICTED-RELOADED"; -console.log(` File 0 reloaded: "${tpl0?.segments?.[0]?.text}"`); -console.log( - ` Result: ${evicted ? "PASS (disk re-read — evicted watcher was closed)" : "FAIL"}`, -); -console.log(); - -// Phase 6: Cleanup -console.log("── Phase 6: Cleanup ──"); -clearUsageBarTemplateCacheForTest(); -await new Promise((r) => { - setTimeout(r, 100); -}); -console.log(` Result: clearUsageBarTemplateCacheForTest called`); -console.log(); - -rmSync(dir, { recursive: true, force: true }); - -console.log("=".repeat(72)); -console.log("VERDICT"); -console.log("=".repeat(72)); -console.log(` ${CAP} files loaded → all cached PASS`); -console.log( - ` 65th file → eviction + watcher close ${evicted ? "PASS" : "FAIL"}`, -); -console.log( - ` Non-evicted watcher still alive ${alive ? "PASS" : "WARN"}`, -); -console.log( - ` ${CAP - 2} files remain cached ${cachedOk === CAP - 2 ? "PASS" : "FAIL"}`, -); -console.log(` cleanup → all watchers closed PASS`); -console.log(); -console.log(` End time: ${new Date().toISOString()}`); -process.exit(evicted && cachedOk === CAP - 2 ? 0 : 1); diff --git a/src/acp/runtime/adapter-contract.testkit.ts b/src/acp/runtime/adapter-contract.testkit.ts deleted file mode 100644 index e5ab1fd202a4..000000000000 --- a/src/acp/runtime/adapter-contract.testkit.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** Shared behavioral contract testkit for ACP runtime adapter implementations. */ -import { randomUUID } from "node:crypto"; -import type { AcpRuntime, AcpRuntimeEvent } from "@openclaw/acp-core/runtime/types"; -import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; -import { expect } from "vitest"; -import { toAcpRuntimeError } from "./errors.js"; - -/** Inputs and optional assertions for the shared ACP runtime adapter contract. */ -type AcpRuntimeAdapterContractParams = { - createRuntime: () => Promise | AcpRuntime; - agentId?: string; - successPrompt?: string; - errorPrompt?: string; - includeControlChecks?: boolean; - assertSuccessEvents?: (events: AcpRuntimeEvent[]) => void | Promise; - assertErrorOutcome?: (params: { - events: AcpRuntimeEvent[]; - thrown: unknown; - }) => void | Promise; -}; - -/** Runs the shared behavioral contract for ACP runtime adapters. */ -export async function runAcpRuntimeAdapterContract( - params: AcpRuntimeAdapterContractParams, -): Promise { - const runtime = await params.createRuntime(); - const sessionKey = `agent:${params.agentId ?? "codex"}:acp:contract-${randomUUID()}`; - const agent = params.agentId ?? "codex"; - - const handle = await runtime.ensureSession({ - sessionKey, - agent, - mode: "persistent", - }); - expect(handle.sessionKey).toBe(sessionKey); - expect(handle.backend.trim()).not.toHaveLength(0); - expect(handle.runtimeSessionName.trim()).not.toHaveLength(0); - - const successEvents: AcpRuntimeEvent[] = []; - for await (const event of runtime.runTurn({ - handle, - text: params.successPrompt ?? "contract-success", - mode: "prompt", - requestId: `contract-success-${randomUUID()}`, - })) { - successEvents.push(event); - } - expect( - successEvents.some( - (event) => - event.type === "done" || - event.type === "text_delta" || - event.type === "status" || - event.type === "tool_call", - ), - ).toBe(true); - expect(successEvents.some((event) => event.type === "done")).toBe(true); - await params.assertSuccessEvents?.(successEvents); - - if (params.includeControlChecks ?? true) { - if (runtime.getStatus) { - const status = await runtime.getStatus({ handle }); - expect(status).toBeDefined(); - expect(typeof status).toBe("object"); - } - if (runtime.setMode) { - await runtime.setMode({ - handle, - mode: "contract", - }); - } - if (runtime.setConfigOption) { - await runtime.setConfigOption({ - handle, - key: "contract_key", - value: "contract_value", - }); - } - } - - let errorThrown: unknown = null; - const errorEvents: AcpRuntimeEvent[] = []; - const errorPrompt = normalizeOptionalString(params.errorPrompt); - if (errorPrompt) { - try { - for await (const event of runtime.runTurn({ - handle, - text: errorPrompt, - mode: "prompt", - requestId: `contract-error-${randomUUID()}`, - })) { - errorEvents.push(event); - } - } catch (error) { - errorThrown = error; - } - const sawErrorEvent = errorEvents.some((event) => event.type === "error"); - expect(Boolean(errorThrown) || sawErrorEvent).toBe(true); - if (errorThrown) { - const acpError = toAcpRuntimeError({ - error: errorThrown, - fallbackCode: "ACP_TURN_FAILED", - fallbackMessage: "ACP runtime contract expected an error turn failure.", - }); - expect(acpError.code.length).toBeGreaterThan(0); - expect(acpError.message.length).toBeGreaterThan(0); - } - } - await params.assertErrorOutcome?.({ - events: errorEvents, - thrown: errorThrown, - }); - - await runtime.cancel({ - handle, - reason: "contract-cancel", - }); - await runtime.close({ - handle, - reason: "contract-close", - }); -} diff --git a/src/agents/anthropic-transport-stream.live.test.ts b/src/agents/anthropic-transport-stream.live.test.ts index 7e3c6345c395..ee3973335954 100644 --- a/src/agents/anthropic-transport-stream.live.test.ts +++ b/src/agents/anthropic-transport-stream.live.test.ts @@ -9,7 +9,7 @@ import type { Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createAnthropicMessagesTransportStreamFn } from "./anthropic-transport-stream.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; -import { isLiveBillingDrift } from "./live-test-provider-drift.js"; +import { isLiveBillingDrift } from "./live-test-provider-drift.test-support.js"; const LIVE = isLiveTestEnabled(["ANTHROPIC_TRANSPORT_LIVE_TEST"]); const describeLive = LIVE ? describe : describe.skip; diff --git a/src/agents/bundle-mcp-shared.test-harness.ts b/src/agents/bundle-mcp-shared.test-harness.ts index 8af99ee1484f..b4faadb9a3e1 100644 --- a/src/agents/bundle-mcp-shared.test-harness.ts +++ b/src/agents/bundle-mcp-shared.test-harness.ts @@ -3,103 +3,10 @@ * fixture files. */ import fs from "node:fs/promises"; -import { createRequire } from "node:module"; import path from "node:path"; -const require = createRequire(import.meta.url); -const SDK_SERVER_MCP_PATH = require.resolve("@modelcontextprotocol/sdk/server/mcp.js"); -const SDK_SERVER_STDIO_PATH = require.resolve("@modelcontextprotocol/sdk/server/stdio.js"); - /** Writes an executable fixture script with parent directories created. */ export async function writeExecutable(filePath: string, content: string): Promise { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, content, { encoding: "utf-8", mode: 0o755 }); } - -/** Writes a stdio MCP server fixture exposing a `bundle_probe` tool. */ -export async function writeBundleProbeMcpServer( - filePath: string, - params: { - startupCounterPath?: string; - startupDelayMs?: number; - pidPath?: string; - exitMarkerPath?: string; - } = {}, -): Promise { - await writeExecutable( - filePath, - `#!/usr/bin/env node -import fs from "node:fs"; -import fsp from "node:fs/promises"; -import { setTimeout as delay } from "node:timers/promises"; -import { McpServer } from ${JSON.stringify(SDK_SERVER_MCP_PATH)}; -import { StdioServerTransport } from ${JSON.stringify(SDK_SERVER_STDIO_PATH)}; - -const startupCounterPath = ${JSON.stringify(params.startupCounterPath ?? "")}; -if (startupCounterPath) { - let current = 0; - try { - current = Number.parseInt((await fsp.readFile(startupCounterPath, "utf8")).trim(), 10) || 0; - } catch {} - await fsp.writeFile(startupCounterPath, String(current + 1), "utf8"); -} -const pidPath = ${JSON.stringify(params.pidPath ?? "")}; -if (pidPath) { - await fsp.writeFile(pidPath, String(process.pid), "utf8"); -} -const exitMarkerPath = ${JSON.stringify(params.exitMarkerPath ?? "")}; -if (exitMarkerPath) { - process.once("exit", () => { - try { - fs.writeFileSync(exitMarkerPath, "exited", "utf8"); - } catch {} - }); -} -const startupDelayMs = ${JSON.stringify(params.startupDelayMs ?? 0)}; -if (startupDelayMs > 0) { - await delay(startupDelayMs); -} - -const server = new McpServer({ name: "bundle-probe", version: "1.0.0" }); -server.tool("bundle_probe", "Bundle MCP probe", async () => { - return { - content: [{ type: "text", text: process.env.BUNDLE_PROBE_TEXT ?? "missing-probe-text" }], - }; -}); - -await server.connect(new StdioServerTransport()); -`, - ); -} - -/** Writes a minimal Claude plugin bundle fixture with a bundled MCP config. */ -export async function writeClaudeBundle(params: { - pluginRoot: string; - serverScriptPath: string; -}): Promise { - await fs.mkdir(path.join(params.pluginRoot, ".claude-plugin"), { recursive: true }); - await fs.writeFile( - path.join(params.pluginRoot, ".claude-plugin", "plugin.json"), - `${JSON.stringify({ name: "bundle-probe" }, null, 2)}\n`, - "utf-8", - ); - await fs.writeFile( - path.join(params.pluginRoot, ".mcp.json"), - `${JSON.stringify( - { - mcpServers: { - bundleProbe: { - command: "node", - args: [path.relative(params.pluginRoot, params.serverScriptPath)], - env: { - BUNDLE_PROBE_TEXT: "FROM-BUNDLE", - }, - }, - }, - }, - null, - 2, - )}\n`, - "utf-8", - ); -} diff --git a/src/agents/embedded-agent-runner-extraparams.live.test.ts b/src/agents/embedded-agent-runner-extraparams.live.test.ts index 25b79f3256c3..c12bac7a0ce2 100644 --- a/src/agents/embedded-agent-runner-extraparams.live.test.ts +++ b/src/agents/embedded-agent-runner-extraparams.live.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { applyExtraParamsToAgent } from "./embedded-agent-runner/extra-params.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; -import { isLiveAuthDrift, isLiveBillingDrift } from "./live-test-provider-drift.js"; +import { isLiveAuthDrift, isLiveBillingDrift } from "./live-test-provider-drift.test-support.js"; const OPENAI_KEY = process.env.OPENAI_API_KEY ?? ""; const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? ""; diff --git a/src/agents/embedded-agent-runner/compact.hooks.harness.ts b/src/agents/embedded-agent-runner/compact.hooks.harness.ts index 3fbe9b84e6b9..10cdb88de653 100644 --- a/src/agents/embedded-agent-runner/compact.hooks.harness.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.harness.ts @@ -77,12 +77,10 @@ export const sessionCompactImpl = vi.fn(async () => ({ details: { ok: true }, })); export const triggerInternalHook: Mock<(event?: unknown) => void> = vi.fn(); -export const sanitizeSessionHistoryMock = vi.fn( +const sanitizeSessionHistoryMock = vi.fn( async (params: { messages: unknown[] }) => params.messages, ); -export const validateReplayTurnsMock = vi.fn( - async ({ messages }: { messages: unknown[] }) => messages, -); +const validateReplayTurnsMock = vi.fn(async ({ messages }: { messages: unknown[] }) => messages); export const getMemorySearchManagerMock: Mock< (params?: unknown) => Promise > = vi.fn(async () => ({ @@ -188,7 +186,7 @@ function createMockToolDefinitions(tools: unknown[] = []) { }); } export const createOpenClawCodingToolsMock = vi.fn(() => []); -export const buildEmbeddedExtensionFactoriesMock = vi.fn(() => []); +const buildEmbeddedExtensionFactoriesMock = vi.fn(() => []); export const guardSessionManagerMock = vi.fn(() => ({ flushPendingToolResults: vi.fn(), })); @@ -243,8 +241,9 @@ function createDefaultCompactionAuthStore(): AuthProfileStore { export const ensureAuthProfileStoreMock: Mock<() => AuthProfileStore> = vi.fn( createDefaultCompactionAuthStore, ); -export const ensureAuthProfileStoreWithoutExternalProfilesMock: Mock<() => AuthProfileStore> = - vi.fn(createDefaultCompactionAuthStore); +const ensureAuthProfileStoreWithoutExternalProfilesMock: Mock<() => AuthProfileStore> = vi.fn( + createDefaultCompactionAuthStore, +); const resolveAgentTransportOverrideMock: Mock<(params?: unknown) => string | undefined> = vi.fn( () => undefined, ); diff --git a/src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts index a9610a5cf1c0..1c6ad23554a4 100644 --- a/src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts @@ -140,8 +140,8 @@ function makeMockRuntimePlan(): MockRuntimePlan { } export const mockedCompactDirect = mockedContextEngine.compact; -export const mockedResolveContextEngine = vi.fn(async () => mockedContextEngine); -export const mockedResolveContextEngineOwnerPluginId = vi.fn(() => undefined); +const mockedResolveContextEngine = vi.fn(async () => mockedContextEngine); +const mockedResolveContextEngineOwnerPluginId = vi.fn(() => undefined); export const mockedBuildAgentRuntimePlan = vi.fn<() => AgentRuntimePlan>( () => makeMockRuntimePlan() as AgentRuntimePlan, ); @@ -159,7 +159,7 @@ function createMockAgentDiscoveryStores(): MockAgentDiscoveryStores { }; } -export const mockedCreateEmptyAgentDiscoveryStores = vi.fn(createMockAgentDiscoveryStores); +const mockedCreateEmptyAgentDiscoveryStores = vi.fn(createMockAgentDiscoveryStores); function createMockResolvedModel( provider = "anthropic", modelId = "test-model", @@ -191,7 +191,7 @@ export const mockedResolveModelAsync = vi.fn( async (provider?: string, modelId?: string, _agentDir?: string, cfg?: unknown) => createMockResolvedModel(provider, modelId, cfg), ); -export const mockedPrepareProviderRuntimeAuth = vi.fn(async () => undefined); +const mockedPrepareProviderRuntimeAuth = vi.fn(async () => undefined); export const mockedRunEmbeddedAttempt = vi.fn<(params: unknown) => Promise>(); export const mockedBuildEmbeddedRunPayloads = vi.fn< @@ -204,7 +204,7 @@ export const mockedWaitForDeferredTurnMaintenanceForSession = vi.fn( async (_sessionKey?: string) => undefined, ); export const mockedSessionLikelyHasOversizedToolResults = vi.fn(() => false); -export const mockedResolveLiveToolResultMaxChars = vi.fn(() => 32_000); +const mockedResolveLiveToolResultMaxChars = vi.fn(() => 32_000); type MockTruncateOversizedToolResultsResult = { truncated: boolean; truncatedCount: number; @@ -264,7 +264,7 @@ export const mockedLog: { isEnabled: vi.fn(() => false), }; -export const mockedFormatBillingErrorMessage = vi.fn(() => ""); +const mockedFormatBillingErrorMessage = vi.fn(() => ""); export const mockedClassifyFailoverReason = vi.fn<(raw: string) => FailoverReason | null>( () => null, ); @@ -277,12 +277,12 @@ export const mockedExtractObservedOverflowTokenCount = vi.fn((msg?: string) => { return match?.[1] ? Number(match[1].replaceAll(",", "")) : undefined; }); export const mockedFormatAssistantErrorText = vi.fn(() => ""); -export const mockedIsAuthAssistantError = vi.fn(() => false); -export const mockedIsBillingAssistantError = vi.fn(() => false); +const mockedIsAuthAssistantError = vi.fn(() => false); +const mockedIsBillingAssistantError = vi.fn(() => false); export const mockedIsCompactionFailureError = vi.fn(() => false); export const mockedIsFailoverAssistantError = vi.fn(() => false); -export const mockedIsFailoverErrorMessage = vi.fn(() => false); -export const mockedIsGenericUnknownStreamErrorMessage = vi.fn((raw: string) => +const mockedIsFailoverErrorMessage = vi.fn(() => false); +const mockedIsGenericUnknownStreamErrorMessage = vi.fn((raw: string) => /^\s*an unknown error occurred\.?\s*$/i.test(raw), ); export const mockedIsLikelyContextOverflowError = vi.fn((msg?: string) => { @@ -294,10 +294,10 @@ export const mockedIsLikelyContextOverflowError = vi.fn((msg?: string) => { lower.includes("prompt is too long") ); }); -export const mockedParseImageSizeError = vi.fn(() => null); -export const mockedParseImageDimensionError = vi.fn(() => null); +const mockedParseImageSizeError = vi.fn(() => null); +const mockedParseImageDimensionError = vi.fn(() => null); export const mockedIsRateLimitAssistantError = vi.fn(() => false); -export const mockedIsTimeoutErrorMessage = vi.fn(() => false); +const mockedIsTimeoutErrorMessage = vi.fn(() => false); export const mockedPickFallbackThinkingLevel = vi.fn<(params?: unknown) => ThinkLevel | null>( () => null, ); @@ -313,11 +313,11 @@ export const mockedResolveContextWindowInfo = vi.fn(() => ({ tokens: 200000, source: "model", })); -export const mockedFormatContextWindowWarningMessage = vi.fn( +const mockedFormatContextWindowWarningMessage = vi.fn( (params: { provider: string; modelId: string; guard: { tokens: number; source: string } }) => `low context window: ${params.provider}/${params.modelId} ctx=${params.guard.tokens} source=${params.guard.source}`, ); -export const mockedFormatContextWindowBlockMessage = vi.fn( +const mockedFormatContextWindowBlockMessage = vi.fn( (params: { guard: { tokens: number; source: string } }) => `Model context window too small (${params.guard.tokens} tokens; source=${params.guard.source}). Minimum is 1000.`, ); @@ -369,7 +369,7 @@ export const mockedResolveAuthProfileOrder = vi.fn<(_params?: unknown) => string type AuthProfileOrderResolution = ReturnType< typeof import("../model-auth.js").resolveAuthProfileOrderWithMetadata >; -export const mockedResolveAuthProfileOrderWithMetadata = vi.fn< +const mockedResolveAuthProfileOrderWithMetadata = vi.fn< (_params?: unknown) => AuthProfileOrderResolution >((params?: unknown) => ({ profileIds: mockedResolveAuthProfileOrder(params), @@ -378,9 +378,9 @@ export const mockedResolveAuthProfileOrderWithMetadata = vi.fn< export const mockedResolveProviderEntryApiKeyProfileReference = vi.fn< (_params?: unknown) => unknown >(() => ({ kind: "none" })); -export const mockedHasUsableCustomProviderApiKey = vi.fn(() => false); +const mockedHasUsableCustomProviderApiKey = vi.fn(() => false); export const mockedMarkAuthProfileSuccess = vi.fn(async () => {}); -export const mockedShouldPreferExplicitConfigApiKeyAuth = vi.fn(() => false); +const mockedShouldPreferExplicitConfigApiKeyAuth = vi.fn(() => false); export const overflowBaseRunParams = { sessionId: "test-session", diff --git a/src/agents/live-cache-regression-policy.ts b/src/agents/live-cache-regression-policy.ts new file mode 100644 index 000000000000..1ccad6c5d1af --- /dev/null +++ b/src/agents/live-cache-regression-policy.ts @@ -0,0 +1,237 @@ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { + LIVE_CACHE_REGRESSION_BASELINE, + type LiveCacheFloor, +} from "./live-cache-regression-baseline.js"; +import { + isLiveCachePrerequisiteSkip, + type LiveResolvedModelPool, + logLiveCache, + resolveLiveDirectModelPool, +} from "./live-cache-test-support.js"; + +const LIVE_CACHE_LANE_RETRIES = 1; +export const LIVE_CACHE_RESPONSE_RETRIES = 2; +const OPENAI_CACHE_PROBE_MIN_MAX_TOKENS = 1024; +const ANTHROPIC_CACHE_PROBE_MIN_MAX_TOKENS = 1024; + +type LiveCacheProviderConfig = Parameters[0]; +type ProviderKey = keyof typeof LIVE_CACHE_REGRESSION_BASELINE; +export type CacheLane = "image" | "mcp" | "stable" | "tool"; +export type CacheUsage = { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; +}; +type BaselineLane = CacheLane | "disabled"; +export type CacheRun = { + hitRate: number; + suffix: string; + text: string; + usage: CacheUsage; +}; +export type LaneResult = { + best?: CacheRun; + disabled?: CacheRun; + warmup?: CacheRun; +}; +export type BaselineFindings = { + regressions: string[]; + warnings: string[]; +}; +type LiveCacheRegressionSummary = Record>; +type LiveCacheProviderResolver = ( + params: LiveCacheProviderConfig, +) => Promise; + +export async function resolveLiveCacheProviderPool(params: { + config: LiveCacheProviderConfig; + regressions: string[]; + resolver?: LiveCacheProviderResolver; + summary: LiveCacheRegressionSummary; + warnings: string[]; +}): Promise { + try { + return await (params.resolver ?? resolveLiveDirectModelPool)(params.config); + } catch (error) { + if (!isLiveCachePrerequisiteSkip(error)) { + throw error; + } + const warning = `${error.provider} skipped: ${error.message}`; + if (error.provider === "openai") { + params.warnings.push(warning); + } else { + params.regressions.push(warning); + } + const providerSummary = params.summary[error.provider]; + if (providerSummary) { + providerSummary.skipped = true; + } + logLiveCache(warning); + return undefined; + } +} + +export function shouldRetryCacheProbeText(params: { + attempt: number; + suffix: string; + text: string; +}): boolean { + const responseTextLower = normalizeLowercaseStringOrEmpty(params.text); + const suffixLower = normalizeLowercaseStringOrEmpty(params.suffix); + const markerLower = `cache-ok ${suffixLower}`; + // Live providers sometimes return near-miss text on the first attempt. + return ( + (!responseTextLower.includes(markerLower) || !responseTextLower.includes(suffixLower)) && + params.attempt <= LIVE_CACHE_RESPONSE_RETRIES + ); +} + +export function resolveCacheProbeMaxTokens(params: { + maxTokens: number | undefined; + providerTag: "anthropic" | "openai"; +}): number { + const requested = params.maxTokens ?? 64; + const floor = + params.providerTag === "anthropic" + ? ANTHROPIC_CACHE_PROBE_MIN_MAX_TOKENS + : OPENAI_CACHE_PROBE_MIN_MAX_TOKENS; + return Math.max(requested, floor); +} + +export function shouldAcceptEmptyCacheProbe(params: { + providerTag: "anthropic" | "openai"; + text: string; + usage: CacheUsage; +}): boolean { + if (params.text.trim().length > 0) { + return false; + } + // Empty text is acceptable only when provider usage proves the cache lane ran. + return ( + (params.usage.input ?? 0) > 0 || + (params.usage.cacheRead ?? 0) > 0 || + (params.usage.cacheWrite ?? 0) > 0 + ); +} + +function resolveBaselineFloor(provider: ProviderKey, lane: string): LiveCacheFloor | undefined { + return LIVE_CACHE_REGRESSION_BASELINE[provider][ + lane as keyof (typeof LIVE_CACHE_REGRESSION_BASELINE)[typeof provider] + ] as LiveCacheFloor | undefined; +} + +function warmupHasCacheEvidence(params: { floor: LiveCacheFloor; warmup: CacheRun }): boolean { + const cacheRead = params.warmup.usage.cacheRead ?? 0; + const cacheWrite = params.warmup.usage.cacheWrite ?? 0; + if (params.floor.minCacheReadOrWrite !== undefined) { + return Math.max(cacheRead, cacheWrite) >= params.floor.minCacheReadOrWrite; + } + if (params.floor.minCacheRead !== undefined && cacheRead < params.floor.minCacheRead) { + return false; + } + if (params.floor.minHitRate !== undefined && params.warmup.hitRate < params.floor.minHitRate) { + return false; + } + return params.floor.minCacheRead !== undefined || params.floor.minHitRate !== undefined; +} + +export function assertAgainstBaseline(params: { + lane: BaselineLane; + provider: ProviderKey; + result: LaneResult; + regressions: string[]; + warnings: string[]; +}): void { + const floor = resolveBaselineFloor(params.provider, params.lane); + const recordRegression = (message: string) => { + // OpenAI cache floors are currently watch-only; Anthropic misses fail. + if (floor?.warnOnly) { + params.warnings.push(message); + } else { + params.regressions.push(message); + } + }; + if (!floor) { + params.regressions.push(`${params.provider}:${params.lane} missing baseline entry`); + return; + } + + if (params.result.best) { + const usage = params.result.best.usage; + if (floor.minCacheReadOrWrite !== undefined) { + const cacheReadOrWrite = Math.max(usage.cacheRead ?? 0, usage.cacheWrite ?? 0); + if (cacheReadOrWrite < floor.minCacheReadOrWrite) { + recordRegression( + `${params.provider}:${params.lane} cacheReadOrWrite=${cacheReadOrWrite} < min=${floor.minCacheReadOrWrite}`, + ); + } + } else if ((usage.cacheRead ?? 0) < (floor.minCacheRead ?? 0)) { + recordRegression( + `${params.provider}:${params.lane} cacheRead=${usage.cacheRead ?? 0} < min=${floor.minCacheRead}`, + ); + } + if (params.result.best.hitRate < (floor.minHitRate ?? 0)) { + recordRegression( + `${params.provider}:${params.lane} hitRate=${params.result.best.hitRate.toFixed(3)} < min=${floor.minHitRate?.toFixed(3)}`, + ); + } + } + + if (params.result.warmup) { + const warmup = params.result.warmup; + const warmupUsage = warmup.usage; + if ( + (warmupUsage.cacheWrite ?? 0) < (floor.minCacheWrite ?? 0) && + !warmupHasCacheEvidence({ floor, warmup }) + ) { + recordRegression( + `${params.provider}:${params.lane} warmup cacheWrite=${warmupUsage.cacheWrite ?? 0} < min=${floor.minCacheWrite}`, + ); + } + } + + if (params.result.disabled) { + const usage = params.result.disabled.usage; + if ((usage.cacheRead ?? 0) > (floor.maxCacheRead ?? Number.POSITIVE_INFINITY)) { + recordRegression( + `${params.provider}:${params.lane} cacheRead=${usage.cacheRead ?? 0} > max=${floor.maxCacheRead}`, + ); + } + if ((usage.cacheWrite ?? 0) > (floor.maxCacheWrite ?? Number.POSITIVE_INFINITY)) { + recordRegression( + `${params.provider}:${params.lane} cacheWrite=${usage.cacheWrite ?? 0} > max=${floor.maxCacheWrite}`, + ); + } + } +} + +export function evaluateAgainstBaseline(params: { + lane: BaselineLane; + provider: ProviderKey; + result: LaneResult; +}): BaselineFindings { + const regressions: string[] = []; + const warnings: string[] = []; + assertAgainstBaseline({ + ...params, + regressions, + warnings, + }); + return { regressions, warnings }; +} + +export function shouldRetryBaselineFindings(findings: BaselineFindings, attempt: number): boolean { + return findings.regressions.length > 0 && attempt <= LIVE_CACHE_LANE_RETRIES; +} + +export function isAnthropicToolProbeDrift(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + return ( + error.message.startsWith("expected tool call for ") || + error.message.startsWith("expected tool-only response for ") + ); +} diff --git a/src/agents/live-cache-regression-runner.test.ts b/src/agents/live-cache-regression-runner.test.ts index 4a0fdb28f016..36a26e6fcd47 100644 --- a/src/agents/live-cache-regression-runner.test.ts +++ b/src/agents/live-cache-regression-runner.test.ts @@ -1,6 +1,15 @@ // Verifies live cache regression baseline classification without live providers. import { describe, expect, it } from "vitest"; -import { testing } from "./live-cache-regression-runner.js"; +import { + assertAgainstBaseline, + evaluateAgainstBaseline, + isAnthropicToolProbeDrift, + resolveCacheProbeMaxTokens, + resolveLiveCacheProviderPool, + shouldAcceptEmptyCacheProbe, + shouldRetryBaselineFindings, + shouldRetryCacheProbeText, +} from "./live-cache-regression-policy.js"; import { LiveCachePrerequisiteSkip, toLiveCachePrerequisiteSkip, @@ -14,7 +23,7 @@ describe("live cache regression runner", () => { const regressions: string[] = []; const warnings: string[] = []; - testing.assertAgainstBaseline({ + assertAgainstBaseline({ lane: "image", provider: "openai", result: { @@ -40,7 +49,7 @@ describe("live cache regression runner", () => { const regressions: string[] = []; const warnings: string[] = []; - testing.assertAgainstBaseline({ + assertAgainstBaseline({ lane: "stable", provider: "openai", result: { @@ -66,7 +75,7 @@ describe("live cache regression runner", () => { // Hard regressions get one rerun to absorb provider cache warmup jitter; // advisory warnings should not trigger reruns. expect( - testing.shouldRetryBaselineFindings( + shouldRetryBaselineFindings( { regressions: ["anthropic:image cacheRead=0 < min=4500"], warnings: [], @@ -75,7 +84,7 @@ describe("live cache regression runner", () => { ), ).toBe(true); expect( - testing.shouldRetryBaselineFindings( + shouldRetryBaselineFindings( { regressions: ["anthropic:image cacheRead=0 < min=4500"], warnings: [], @@ -84,7 +93,7 @@ describe("live cache regression runner", () => { ), ).toBe(false); expect( - testing.shouldRetryBaselineFindings( + shouldRetryBaselineFindings( { regressions: [], warnings: ["openai:image cacheRead=0 < min=3840"], @@ -102,7 +111,7 @@ describe("live cache regression runner", () => { openai: {}, }; - const resolved = await testing.resolveLiveCacheProviderPool({ + const resolved = await resolveLiveCacheProviderPool({ config: { provider: "openai", api: "openai-responses", @@ -138,7 +147,7 @@ describe("live cache regression runner", () => { openai: {}, }; - const resolved = await testing.resolveLiveCacheProviderPool({ + const resolved = await resolveLiveCacheProviderPool({ config: { provider: "anthropic", api: "anthropic-messages", @@ -177,35 +186,35 @@ describe("live cache regression runner", () => { it("retries a cache probe twice when provider text misses the sentinel", () => { expect( - testing.shouldRetryCacheProbeText({ + shouldRetryCacheProbeText({ attempt: 1, suffix: "openai-stable-hit-a", text: "", }), ).toBe(true); expect( - testing.shouldRetryCacheProbeText({ + shouldRetryCacheProbeText({ attempt: 2, suffix: "openai-stable-hit-a", text: "", }), ).toBe(true); expect( - testing.shouldRetryCacheProbeText({ + shouldRetryCacheProbeText({ attempt: 3, suffix: "openai-stable-hit-a", text: "", }), ).toBe(false); expect( - testing.shouldRetryCacheProbeText({ + shouldRetryCacheProbeText({ attempt: 1, suffix: "openai-stable-hit-a", text: "I saw openai-stable-hit-a.", }), ).toBe(true); expect( - testing.shouldRetryCacheProbeText({ + shouldRetryCacheProbeText({ attempt: 1, suffix: "openai-stable-hit-a", text: "CACHE-OK openai-stable-hit-a", @@ -215,25 +224,25 @@ describe("live cache regression runner", () => { it("keeps cache probes above the provider empty-output floor", () => { expect( - testing.resolveCacheProbeMaxTokens({ + resolveCacheProbeMaxTokens({ maxTokens: 32, providerTag: "openai", }), ).toBe(1024); expect( - testing.resolveCacheProbeMaxTokens({ + resolveCacheProbeMaxTokens({ maxTokens: 512, providerTag: "openai", }), ).toBe(1024); expect( - testing.resolveCacheProbeMaxTokens({ + resolveCacheProbeMaxTokens({ maxTokens: 2048, providerTag: "openai", }), ).toBe(2048); expect( - testing.resolveCacheProbeMaxTokens({ + resolveCacheProbeMaxTokens({ maxTokens: 32, providerTag: "anthropic", }), @@ -241,46 +250,44 @@ describe("live cache regression runner", () => { }); it("classifies Anthropic tool-only probe misses as provider drift", () => { - expect(testing.isAnthropicToolProbeDrift(new Error("expected tool call for noop"))).toBe(true); + expect(isAnthropicToolProbeDrift(new Error("expected tool call for noop"))).toBe(true); expect( - testing.isAnthropicToolProbeDrift( - new Error('expected tool-only response for noop, got "ok"'), - ), + isAnthropicToolProbeDrift(new Error('expected tool-only response for noop, got "ok"')), ).toBe(true); - expect(testing.isAnthropicToolProbeDrift(new Error("other failure"))).toBe(false); + expect(isAnthropicToolProbeDrift(new Error("other failure"))).toBe(false); }); it("accepts empty cache probe text only when usage is observable", () => { expect( - testing.shouldAcceptEmptyCacheProbe({ + shouldAcceptEmptyCacheProbe({ providerTag: "openai", text: "", usage: { input: 5_000 }, }), ).toBe(true); expect( - testing.shouldAcceptEmptyCacheProbe({ + shouldAcceptEmptyCacheProbe({ providerTag: "openai", text: "", usage: { cacheRead: 4_608 }, }), ).toBe(true); expect( - testing.shouldAcceptEmptyCacheProbe({ + shouldAcceptEmptyCacheProbe({ providerTag: "openai", text: "wrong", usage: { input: 5_000 }, }), ).toBe(false); expect( - testing.shouldAcceptEmptyCacheProbe({ + shouldAcceptEmptyCacheProbe({ providerTag: "anthropic", text: "", usage: { input: 5_000 }, }), ).toBe(true); expect( - testing.shouldAcceptEmptyCacheProbe({ + shouldAcceptEmptyCacheProbe({ providerTag: "openai", text: "", usage: {}, @@ -289,7 +296,7 @@ describe("live cache regression runner", () => { }); it("accepts a warmup that already hits the provider cache", () => { - const findings = testing.evaluateAgainstBaseline({ + const findings = evaluateAgainstBaseline({ lane: "image", provider: "anthropic", result: { @@ -314,7 +321,7 @@ describe("live cache regression runner", () => { it("still rejects warmups with no cache write or cache hit evidence", () => { // A successful best probe is not enough: warmup must prove either cache // write or read evidence so the measured hit is meaningful. - const findings = testing.evaluateAgainstBaseline({ + const findings = evaluateAgainstBaseline({ lane: "image", provider: "anthropic", result: { diff --git a/src/agents/live-cache-regression-runner.ts b/src/agents/live-cache-regression-runner.ts index cfe0d12acc7a..36120a846282 100644 --- a/src/agents/live-cache-regression-runner.ts +++ b/src/agents/live-cache-regression-runner.ts @@ -11,19 +11,28 @@ import { Type } from "typebox"; import type { AssistantMessage, Message, Tool } from "../llm/types.js"; import { extractAssistantText } from "./embedded-agent-utils.js"; import { - LIVE_CACHE_REGRESSION_BASELINE, - type LiveCacheFloor, -} from "./live-cache-regression-baseline.js"; + assertAgainstBaseline, + type BaselineFindings, + type CacheLane, + type CacheRun, + type CacheUsage, + evaluateAgainstBaseline, + isAnthropicToolProbeDrift, + type LaneResult, + LIVE_CACHE_RESPONSE_RETRIES, + resolveCacheProbeMaxTokens, + resolveLiveCacheProviderPool, + shouldAcceptEmptyCacheProbe, + shouldRetryBaselineFindings, + shouldRetryCacheProbeText, +} from "./live-cache-regression-policy.js"; import { buildAssistantHistoryTurn, buildStableCachePrefix, completeSimpleWithLiveTimeout, computeCacheHitRate, - isLiveCachePrerequisiteSkip, type LiveResolvedModel, - type LiveResolvedModelPool, logLiveCache, - resolveLiveDirectModelPool, withLiveDirectModelApiKey, } from "./live-cache-test-support.js"; import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js"; @@ -31,10 +40,7 @@ import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js"; const OPENAI_TIMEOUT_MS = 120_000; const ANTHROPIC_TIMEOUT_MS = 120_000; const LIVE_CACHE_LANE_RETRIES = 1; -const LIVE_CACHE_RESPONSE_RETRIES = 2; const OPENAI_CACHE_REASONING = "none" as unknown as never; -const OPENAI_CACHE_PROBE_MIN_MAX_TOKENS = 1024; -const ANTHROPIC_CACHE_PROBE_MIN_MAX_TOKENS = 1024; const OPENAI_PREFIX = buildStableCachePrefix("openai"); const OPENAI_MCP_PREFIX = buildStableCachePrefix("openai-mcp-style"); const ANTHROPIC_PREFIX = buildStableCachePrefix("anthropic"); @@ -43,42 +49,11 @@ const LIVE_TEST_PNG_URL = new URL( import.meta.url, ); -type LiveCacheProviderConfig = Parameters[0]; -type ProviderKey = keyof typeof LIVE_CACHE_REGRESSION_BASELINE; -type CacheLane = "image" | "mcp" | "stable" | "tool"; -type CacheUsage = { - input?: number; - output?: number; - cacheRead?: number; - cacheWrite?: number; -}; -type BaselineLane = CacheLane | "disabled"; -type CacheRun = { - hitRate: number; - suffix: string; - text: string; - usage: CacheUsage; -}; -type LaneResult = { - best?: CacheRun; - disabled?: CacheRun; - warmup?: CacheRun; -}; -type BaselineFindings = { - regressions: string[]; - warnings: string[]; -}; - type LiveCacheRegressionResult = { regressions: string[]; summary: Record>; warnings: string[]; }; -type LiveCacheRegressionSummary = LiveCacheRegressionResult["summary"]; -type LiveCacheProviderResolver = ( - params: LiveCacheProviderConfig, -) => Promise; - class CacheProbeTextMismatchError extends Error { constructor( readonly suffix: string, @@ -108,34 +83,6 @@ function makeUserTurn(content: Extract["content"]): M }; } -async function resolveLiveCacheProviderPool(params: { - config: LiveCacheProviderConfig; - regressions: string[]; - resolver?: LiveCacheProviderResolver; - summary: LiveCacheRegressionSummary; - warnings: string[]; -}): Promise { - try { - return await (params.resolver ?? resolveLiveDirectModelPool)(params.config); - } catch (error) { - if (!isLiveCachePrerequisiteSkip(error)) { - throw error; - } - const warning = `${error.provider} skipped: ${error.message}`; - if (error.provider === "openai") { - params.warnings.push(warning); - } else { - params.regressions.push(warning); - } - const providerSummary = params.summary[error.provider]; - if (providerSummary) { - providerSummary.skipped = true; - } - logLiveCache(warning); - return undefined; - } -} - function makeImageUserTurn(text: string, pngBase64: string): Message { return makeUserTurn([ { type: "text", text }, @@ -174,61 +121,12 @@ function normalizeCacheUsage(usage: AssistantMessage["usage"] | undefined): Cach }; } -function resolveBaselineFloor(provider: ProviderKey, lane: string): LiveCacheFloor | undefined { - return LIVE_CACHE_REGRESSION_BASELINE[provider][ - lane as keyof (typeof LIVE_CACHE_REGRESSION_BASELINE)[typeof provider] - ] as LiveCacheFloor | undefined; -} - function assert(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(message); } } -function shouldRetryCacheProbeText(params: { - attempt: number; - suffix: string; - text: string; -}): boolean { - const responseTextLower = normalizeLowercaseStringOrEmpty(params.text); - const suffixLower = normalizeLowercaseStringOrEmpty(params.suffix); - const markerLower = `cache-ok ${suffixLower}`; - // Live providers sometimes return near-miss text on the first attempt. - return ( - (!responseTextLower.includes(markerLower) || !responseTextLower.includes(suffixLower)) && - params.attempt <= LIVE_CACHE_RESPONSE_RETRIES - ); -} - -function resolveCacheProbeMaxTokens(params: { - maxTokens: number | undefined; - providerTag: "anthropic" | "openai"; -}): number { - const requested = params.maxTokens ?? 64; - const floor = - params.providerTag === "anthropic" - ? ANTHROPIC_CACHE_PROBE_MIN_MAX_TOKENS - : OPENAI_CACHE_PROBE_MIN_MAX_TOKENS; - return Math.max(requested, floor); -} - -function shouldAcceptEmptyCacheProbe(params: { - providerTag: "anthropic" | "openai"; - text: string; - usage: CacheUsage; -}): boolean { - if (params.text.trim().length > 0) { - return false; - } - // Empty text is acceptable only when provider usage proves the cache lane ran. - return ( - (params.usage.input ?? 0) > 0 || - (params.usage.cacheRead ?? 0) > 0 || - (params.usage.cacheWrite ?? 0) > 0 - ); -} - async function runToolOnlyTurn(params: { apiKey: string; cacheRetention: "none" | "short" | "long"; @@ -490,110 +388,6 @@ function formatUsage(usage: CacheUsage | undefined) { return `cacheRead=${usage?.cacheRead ?? 0} cacheWrite=${usage?.cacheWrite ?? 0} input=${usage?.input ?? 0} output=${usage?.output ?? 0}`; } -function warmupHasCacheEvidence(params: { floor: LiveCacheFloor; warmup: CacheRun }): boolean { - const cacheRead = params.warmup.usage.cacheRead ?? 0; - const cacheWrite = params.warmup.usage.cacheWrite ?? 0; - if (params.floor.minCacheReadOrWrite !== undefined) { - return Math.max(cacheRead, cacheWrite) >= params.floor.minCacheReadOrWrite; - } - if (params.floor.minCacheRead !== undefined && cacheRead < params.floor.minCacheRead) { - return false; - } - if (params.floor.minHitRate !== undefined && params.warmup.hitRate < params.floor.minHitRate) { - return false; - } - return params.floor.minCacheRead !== undefined || params.floor.minHitRate !== undefined; -} - -function assertAgainstBaseline(params: { - lane: BaselineLane; - provider: ProviderKey; - result: LaneResult; - regressions: string[]; - warnings: string[]; -}) { - const floor = resolveBaselineFloor(params.provider, params.lane); - const recordRegression = (message: string) => { - // OpenAI cache floors are currently watch-only; Anthropic misses fail. - if (floor?.warnOnly) { - params.warnings.push(message); - } else { - params.regressions.push(message); - } - }; - if (!floor) { - params.regressions.push(`${params.provider}:${params.lane} missing baseline entry`); - return; - } - - if (params.result.best) { - const usage = params.result.best.usage; - if (floor.minCacheReadOrWrite !== undefined) { - const cacheReadOrWrite = Math.max(usage.cacheRead ?? 0, usage.cacheWrite ?? 0); - if (cacheReadOrWrite < floor.minCacheReadOrWrite) { - recordRegression( - `${params.provider}:${params.lane} cacheReadOrWrite=${cacheReadOrWrite} < min=${floor.minCacheReadOrWrite}`, - ); - } - } else if ((usage.cacheRead ?? 0) < (floor.minCacheRead ?? 0)) { - recordRegression( - `${params.provider}:${params.lane} cacheRead=${usage.cacheRead ?? 0} < min=${floor.minCacheRead}`, - ); - } - if (params.result.best.hitRate < (floor.minHitRate ?? 0)) { - recordRegression( - `${params.provider}:${params.lane} hitRate=${params.result.best.hitRate.toFixed(3)} < min=${floor.minHitRate?.toFixed(3)}`, - ); - } - } - - if (params.result.warmup) { - const warmup = params.result.warmup; - const warmupUsage = warmup.usage; - if ( - (warmupUsage.cacheWrite ?? 0) < (floor.minCacheWrite ?? 0) && - !warmupHasCacheEvidence({ floor, warmup }) - ) { - recordRegression( - `${params.provider}:${params.lane} warmup cacheWrite=${warmupUsage.cacheWrite ?? 0} < min=${floor.minCacheWrite}`, - ); - } - } - - if (params.result.disabled) { - const usage = params.result.disabled.usage; - if ((usage.cacheRead ?? 0) > (floor.maxCacheRead ?? Number.POSITIVE_INFINITY)) { - recordRegression( - `${params.provider}:${params.lane} cacheRead=${usage.cacheRead ?? 0} > max=${floor.maxCacheRead}`, - ); - } - if ((usage.cacheWrite ?? 0) > (floor.maxCacheWrite ?? Number.POSITIVE_INFINITY)) { - recordRegression( - `${params.provider}:${params.lane} cacheWrite=${usage.cacheWrite ?? 0} > max=${floor.maxCacheWrite}`, - ); - } - } -} - -function evaluateAgainstBaseline(params: { - lane: BaselineLane; - provider: ProviderKey; - result: LaneResult; -}): BaselineFindings { - const regressions: string[] = []; - const warnings: string[] = []; - assertAgainstBaseline({ - ...params, - regressions, - warnings, - }); - return { regressions, warnings }; -} - -function shouldRetryBaselineFindings(findings: BaselineFindings, attempt: number): boolean { - return findings.regressions.length > 0 && attempt <= LIVE_CACHE_LANE_RETRIES; -} - async function runRepeatedLaneWithBaselineRetry(params: { lane: CacheLane; providerTag: "anthropic" | "openai"; @@ -651,16 +445,6 @@ function isAnthropicEmptyCacheProbe(error: unknown): boolean { return error instanceof CacheProbeTextMismatchError && error.text.trim().length === 0; } -function isAnthropicToolProbeDrift(error: unknown): boolean { - if (!(error instanceof Error)) { - return false; - } - return ( - error.message.startsWith("expected tool call for ") || - error.message.startsWith("expected tool-only response for ") - ); -} - function shouldSkipAnthropicCacheProviderDrift(error: unknown): boolean { return Boolean( shouldSkipLiveProviderDrift({ @@ -743,18 +527,6 @@ async function runAnthropicDisabledCacheLane(params: { } } -/** Internal seams used by unit tests for baseline and retry decisions. */ -export const testing = { - assertAgainstBaseline, - evaluateAgainstBaseline, - resolveLiveCacheProviderPool, - resolveCacheProbeMaxTokens, - isAnthropicToolProbeDrift, - shouldAcceptEmptyCacheProbe, - shouldRetryCacheProbeText, - shouldRetryBaselineFindings, -}; - /** Runs all live prompt-cache lanes and returns hard regressions plus warn-only drift. */ export async function runLiveCacheRegression(): Promise { const pngBase64 = (await fs.readFile(LIVE_TEST_PNG_URL)).toString("base64"); @@ -881,4 +653,3 @@ export async function runLiveCacheRegression(): Promise { it("classifies provider account drift", () => { diff --git a/src/agents/live-test-provider-drift.ts b/src/agents/live-test-provider-drift.ts index 399015aeac30..373bd9f9b0d4 100644 --- a/src/agents/live-test-provider-drift.ts +++ b/src/agents/live-test-provider-drift.ts @@ -63,7 +63,7 @@ function isAnthropicBillingError(message: string): boolean { } /** Returns whether an error is expected live auth/account drift. */ -export function isLiveAuthDrift(error: unknown): boolean { +function isLiveAuthDrift(error: unknown): boolean { const raw = liveProviderErrorText(error); const message = normalizeLowercaseStringOrEmpty(raw); return ( @@ -74,13 +74,13 @@ export function isLiveAuthDrift(error: unknown): boolean { } /** Returns whether an error is expected live billing/quota drift. */ -export function isLiveBillingDrift(error: unknown): boolean { +function isLiveBillingDrift(error: unknown): boolean { const raw = liveProviderErrorText(error); return isBillingErrorMessage(raw) || isAnthropicBillingError(raw); } /** Returns whether an error is expected live rate-limit drift. */ -export function isLiveRateLimitDrift(error: unknown): boolean { +function isLiveRateLimitDrift(error: unknown): boolean { const raw = liveProviderErrorText(error); return isRateLimitErrorMessage(raw) || isApiKeyRateLimitError(raw); } @@ -96,7 +96,7 @@ function isLiveModelNotFoundDrift(error: unknown): boolean { } /** Returns whether an error is expected upstream/provider availability drift. */ -export function isLiveProviderUnavailableDrift(error: unknown): boolean { +function isLiveProviderUnavailableDrift(error: unknown): boolean { const raw = liveProviderErrorText(error); const htmlCandidate = raw.trim().replace(/^error:\s*/i, ""); const msg = normalizeLowercaseStringOrEmpty(raw); diff --git a/src/agents/models.profiles.live.test.ts b/src/agents/models.profiles.live.test.ts index a3cb74333399..5768eebe45b7 100644 --- a/src/agents/models.profiles.live.test.ts +++ b/src/agents/models.profiles.live.test.ts @@ -58,11 +58,11 @@ import { requiresLiveProfileCredential, resolveLiveCredentialPrecedence, } from "./live-test-helpers.js"; +import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js"; import { isLiveBillingDrift, isLiveRateLimitDrift, - shouldSkipLiveProviderDrift, -} from "./live-test-provider-drift.js"; +} from "./live-test-provider-drift.test-support.js"; import { getApiKeyForModel, requireApiKey, diff --git a/src/agents/openai-transport-stream.test-harness.ts b/src/agents/openai-transport-stream.test-harness.ts index d60dc00a3ea0..a1c3a435dd98 100644 --- a/src/agents/openai-transport-stream.test-harness.ts +++ b/src/agents/openai-transport-stream.test-harness.ts @@ -15,7 +15,7 @@ export type OpenAICompletionsOutput = Parameters[1]; -export type ResponsesApi = Extract< +type ResponsesApi = Extract< Api, "openai-responses" | "openai-chatgpt-responses" | "azure-openai-responses" >; diff --git a/src/agents/test-helpers/fast-tool-stubs.ts b/src/agents/test-helpers/fast-tool-stubs.ts index 02f048803f45..44319f5b9784 100644 --- a/src/agents/test-helpers/fast-tool-stubs.ts +++ b/src/agents/test-helpers/fast-tool-stubs.ts @@ -5,7 +5,7 @@ */ import { vi } from "vitest"; -export type StubTool = { +type StubTool = { name: string; description: string; parameters: { type: "object"; properties: Record }; diff --git a/src/agents/tools/image-tool.providers.live.test.ts b/src/agents/tools/image-tool.providers.live.test.ts index b7026a4a65c4..e680b2e55a9f 100644 --- a/src/agents/tools/image-tool.providers.live.test.ts +++ b/src/agents/tools/image-tool.providers.live.test.ts @@ -20,7 +20,7 @@ import { isServerErrorMessage, } from "../../plugin-sdk/test-env.js"; import { isLiveTestEnabled } from "../live-test-helpers.js"; -import { isLiveAuthDrift } from "../live-test-provider-drift.js"; +import { isLiveAuthDrift } from "../live-test-provider-drift.test-support.js"; import { createImageTool } from "./image-tool.js"; import { testing } from "./image-tool.test-support.js"; diff --git a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts index 09c6b4bead2a..a6f18b215713 100644 --- a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts @@ -369,14 +369,12 @@ export { acpManagerRuntimeMocks, acpMocks, agentEventMocks, - conversationBindingMocks, diagnosticMocks, globalMocks, hookMocks, internalHookMocks, messageAuditMocks, mocks, - pluginConversationBindingMocks, replyMediaPathMocks, runtimePluginMocks, sessionBindingMocks, diff --git a/src/auto-reply/reply/dispatch-from-config.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.test-harness.ts index 04ee065fcbcd..a95c8dd0781e 100644 --- a/src/auto-reply/reply/dispatch-from-config.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.test-harness.ts @@ -73,7 +73,7 @@ export let dispatchReplyFromConfig: typeof import("./dispatch-from-config.js").d export let dispatchFromConfigTesting: typeof import("./dispatch-from-config.test-support.js").testing; -export let resetInboundDedupe: typeof import("./inbound-dedupe.js").resetInboundDedupe; +let resetInboundDedupe: typeof import("./inbound-dedupe.js").resetInboundDedupe; export let tryDispatchAcpReplyHook: typeof import("../../plugin-sdk/acp-runtime.js").tryDispatchAcpReplyHook; @@ -81,17 +81,17 @@ export let createReplyOperation: typeof import("./reply-run-registry.js").create export let replyRunRegistry: typeof import("./reply-run-registry.js").replyRunRegistry; -export let replyRunTesting: typeof import("./reply-run-registry.test-support.js").testing; +let replyRunTesting: typeof import("./reply-run-registry.test-support.js").testing; export let admitReplyTurn: typeof import("./reply-turn-admission.js").admitReplyTurn; export let runWithReplyOperationLifecycleAdmission: typeof import("./reply-turn-admission.js").runWithReplyOperationLifecycleAdmission; -export type DispatchReplyArgs = Parameters< +type DispatchReplyArgs = Parameters< typeof import("./dispatch-from-config.js").dispatchReplyFromConfig >[0]; -export function shouldUseAcpReplyDispatchHook(eventUnknown: unknown): boolean { +function shouldUseAcpReplyDispatchHook(eventUnknown: unknown): boolean { const event = eventUnknown as { sessionKey?: string; ctx?: { @@ -115,7 +115,7 @@ export function setNoAbort() { mocks.tryFastAbortFromMessage.mockResolvedValue(noAbortResult); } -export type MockAcpRuntime = AcpRuntime & { +type MockAcpRuntime = AcpRuntime & { ensureSession: Mock<(input: AcpRuntimeEnsureInput) => Promise>; runTurn: Mock<(input: AcpRuntimeTurnInput) => AsyncIterable>; cancel: Mock<(input: { handle: AcpRuntimeHandle; reason?: string }) => Promise>; @@ -148,7 +148,7 @@ export function createAcpRuntime(events: AcpRuntimeEvent[]): MockAcpRuntime { return runtime as MockAcpRuntime; } -export function createMockAcpSessionManager() { +function createMockAcpSessionManager() { return { resolveSession: (params: { cfg: OpenClawConfig; sessionKey: string }) => { const entry = acpMocks.readAcpSessionEntry({ diff --git a/src/auto-reply/reply/session-entry-persistence.ts b/src/auto-reply/reply/session-entry-persistence.ts index c5019256ec87..afa514784f09 100644 --- a/src/auto-reply/reply/session-entry-persistence.ts +++ b/src/auto-reply/reply/session-entry-persistence.ts @@ -18,7 +18,7 @@ type PersistReplySessionEntryParams = { touchedFields?: ReadonlyArray; }; -export type PersistReplySessionEntryResult = +type PersistReplySessionEntryResult = | { status: "current"; entry: SessionEntry } | { status: "lifecycle-invalidated"; error: string; entry?: SessionEntry }; diff --git a/src/channels/plugins/contracts/test-helpers/group-policy-contract-suites.ts b/src/channels/plugins/contracts/test-helpers/group-policy-contract-suites.ts index 1e36199eb059..e79a84cd2884 100644 --- a/src/channels/plugins/contracts/test-helpers/group-policy-contract-suites.ts +++ b/src/channels/plugins/contracts/test-helpers/group-policy-contract-suites.ts @@ -6,7 +6,7 @@ import { expect, it } from "vitest"; import { resolveOpenProviderRuntimeGroupPolicy } from "../../../../config/runtime-group-policy.js"; -export type RuntimeGroupPolicyResolver = ( +type RuntimeGroupPolicyResolver = ( params: Parameters[0], ) => ReturnType; diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index ab5094f2569b..997b5cbda96f 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -93,7 +93,7 @@ export const applyPluginUninstallDirectoryRemoval: AsyncUnknownMock = vi.fn(); export const updateNpmInstalledPlugins: Mock = vi.fn(); export const updateNpmInstalledHookPacks: Mock = vi.fn(); export const promptYesNo: AsyncUnknownMock = vi.fn(); -export const promptText: AsyncUnknownMock = vi.fn(); +const promptText: AsyncUnknownMock = vi.fn(); export class PromptInputClosedError extends Error { constructor() { super("Prompt input closed before an answer was received."); diff --git a/src/commands/test-wizard-helpers.ts b/src/commands/test-wizard-helpers.ts index 181a93a97967..d7ccd9462bd0 100644 --- a/src/commands/test-wizard-helpers.ts +++ b/src/commands/test-wizard-helpers.ts @@ -6,6 +6,5 @@ export { createExitThrowingRuntime, createWizardPrompter, readAuthProfilesForAgent, - requireOpenClawAgentDir, setupAuthTestEnv, } from "../../test/helpers/auth-wizard.js"; diff --git a/src/config/sessions/test-helpers.ts b/src/config/sessions/test-helpers.ts index 0362ab32aba3..8de85c910f47 100644 --- a/src/config/sessions/test-helpers.ts +++ b/src/config/sessions/test-helpers.ts @@ -3,9 +3,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach } from "vitest"; -import { loadSessionStore } from "./store-load.js"; -import { clearSessionStoreCacheForTest } from "./store-writer-state.js"; -import type { SessionEntry } from "./types.js"; /** Creates and cleans a temporary session store fixture around each test. */ export function useTempSessionsFixture(prefix: string) { @@ -29,23 +26,3 @@ export function useTempSessionsFixture(prefix: string) { sessionsDir: () => sessionsDir, }; } - -export function writeSessionStoreForTest(storePath: string, store: Record): void { - fs.mkdirSync(path.dirname(storePath), { recursive: true }); - clearSessionStoreCacheForTest(); - fs.writeFileSync(storePath, `${JSON.stringify(store, null, 2)}\n`, "utf8"); - clearSessionStoreCacheForTest(); -} - -export async function writeSessionStoreForTestAsync( - storePath: string, - store: Record, -): Promise { - writeSessionStoreForTest(storePath, store); -} - -export function readSessionStoreForTest( - storePath: string, -): Record { - return loadSessionStore(storePath, { skipCache: true }) as Record; -} diff --git a/src/config/sessions/transcript-append.test-support.ts b/src/config/sessions/transcript-append.test-support.ts index 3e31d589ce83..99a1f7894931 100644 --- a/src/config/sessions/transcript-append.test-support.ts +++ b/src/config/sessions/transcript-append.test-support.ts @@ -417,7 +417,7 @@ async function resolveTranscriptAppendQueueKey(transcriptPath: string): Promise< } } -export async function withSessionTranscriptAppendQueue( +async function withSessionTranscriptAppendQueue( transcriptPath: string, fn: () => Promise, ): Promise { @@ -427,7 +427,7 @@ export async function withSessionTranscriptAppendQueue( return await transcriptAppendQueue.enqueue(queueKey, fn); } -export type AppendSessionTranscriptMessageParams = { +type AppendSessionTranscriptMessageParams = { transcriptPath: string; message: TMessage; now?: number; @@ -443,7 +443,7 @@ export type AppendSessionTranscriptMessageParams = { onHeaderCreated?: (serializedHeader: string) => void; }; -export type AppendSessionTranscriptMessageResult = { +type AppendSessionTranscriptMessageResult = { messageId: string; message: TMessage; appended: boolean; diff --git a/src/cron/isolated-agent.turn-test-helpers.ts b/src/cron/isolated-agent.turn-test-helpers.ts index 0c9894ac57f9..f536469cb1f0 100644 --- a/src/cron/isolated-agent.turn-test-helpers.ts +++ b/src/cron/isolated-agent.turn-test-helpers.ts @@ -1,6 +1,6 @@ /** Reusable turn-level fixtures for isolated cron agent regression tests. */ import "./isolated-agent.mocks.js"; -import { expect, vi } from "vitest"; +import { vi } from "vitest"; import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import type { CliDeps } from "../cli/deps.js"; import { loadSessionEntry } from "../config/sessions/session-accessor.js"; @@ -44,21 +44,6 @@ export function mockEmbeddedOk() { mockEmbeddedTexts(["ok"]); } -export function expectEmbeddedProviderModel(expected: { provider: string; model: string }) { - const call = vi.mocked(runEmbeddedAgent).mock.calls.at(-1)?.[0] as { - provider?: string; - model?: string; - }; - return { - provider: call?.provider, - model: call?.model, - assert() { - expect(call?.provider).toBe(expected.provider); - expect(call?.model).toBe(expected.model); - }, - }; -} - export async function readSessionEntry(storePath: string, key: string) { return loadSessionEntry({ storePath, sessionKey: key }); } @@ -118,42 +103,3 @@ export async function runCronTurn(home: string, options: RunCronTurnOptions = {} return { deps, res, storePath }; } - -export async function runGmailHookTurn( - home: string, - storeEntries?: Record>, -) { - return runCronTurn(home, { - cfgOverrides: { - hooks: { - gmail: { - model: GMAIL_MODEL, - }, - }, - }, - jobPayload: DEFAULT_AGENT_TURN_PAYLOAD, - sessionKey: "hook:gmail:msg-1", - storeEntries, - }); -} - -export async function runTurnWithStoredModelOverride( - home: string, - jobPayload: CronJob["payload"], - modelOverride = "gpt-4.1-mini", - providerOverride = "openai", - cfgOverrides?: Parameters[2], -) { - return runCronTurn(home, { - cfgOverrides, - jobPayload, - storeEntries: { - "agent:main:cron:job-1": { - sessionId: "existing-cron-session", - updatedAt: Date.now(), - providerOverride, - modelOverride, - }, - }, - }); -} diff --git a/src/cron/isolated-agent/run.test-harness.ts b/src/cron/isolated-agent/run.test-harness.ts index 220ff073fc02..6881d3d20f99 100644 --- a/src/cron/isolated-agent/run.test-harness.ts +++ b/src/cron/isolated-agent/run.test-harness.ts @@ -52,16 +52,16 @@ function usesRealAccessorStore(storePath?: string): boolean { export const buildWorkspaceSkillSnapshotMock = createMock(); export const resolveAgentConfigMock = createMock(); -export const resolveEffectiveModelFallbacksMock = createMock(); -export const resolveSubagentModelFallbacksOverrideMock = createMock(); +const resolveEffectiveModelFallbacksMock = createMock(); +const resolveSubagentModelFallbacksOverrideMock = createMock(); export const resolveAgentModelFallbacksOverrideMock = createMock(); export const resolveAgentSkillsFilterMock = createMock(); -export const getModelRefStatusMock = createMock(); +const getModelRefStatusMock = createMock(); export const isCliProviderMock = createMock(); export const resolveCliRuntimeExecutionProviderMock = createMock(); export const resolveAllowedModelRefMock = createMock(); export const resolveConfiguredModelRefMock = createMock(); -export const resolveHooksGmailModelMock = createMock(); +const resolveHooksGmailModelMock = createMock(); export const resolveThinkingDefaultMock = createMock(); export const resolveEffectiveAgentRuntimeMock = createMock(); export const runWithModelFallbackMock = createMock(); @@ -69,11 +69,11 @@ export const runEmbeddedAgentMock = createMock(); export const runCliAgentMock = createMock(); export const lookupContextTokensMock = createMock(); export const getCliSessionBindingMock = createMock(); -export const getCliSessionIdMock = createMock(); +const getCliSessionIdMock = createMock(); export const clearCliSessionMock = createMock(); export const setCliSessionBindingMock = createMock(); export const loadSessionEntryMock = createMock(); -export const replaceSessionEntryMock = createMock(); +const replaceSessionEntryMock = createMock(); export const patchSessionEntryMock = createMock(); export const resolveCronSessionMock = createMock(); export const logWarnMock = createMock(); @@ -88,7 +88,7 @@ export const queueCronMessageToolDeliveryAwarenessMock = createMock(); export const cleanupDirectCronSessionMock = createMock(); export const preflightCronModelProviderMock = createMock(); export const isHeartbeatOnlyResponseMock = createMock(); -export const resolveHeartbeatAckMaxCharsMock = createMock(); +const resolveHeartbeatAckMaxCharsMock = createMock(); export const resolveSessionAuthProfileOverrideMock = createMock(); export const resolveFastModeStateMock = createMock(); export const getChannelPluginMock = createMock(); diff --git a/src/cron/service.issue-regressions.test-helpers.ts b/src/cron/service.issue-regressions.test-helpers.ts index c30efda01715..57d6ac64b3a3 100644 --- a/src/cron/service.issue-regressions.test-helpers.ts +++ b/src/cron/service.issue-regressions.test-helpers.ts @@ -4,13 +4,7 @@ import { createDefaultIsolatedRunner, noopLogger, setupCronRegressionFixtures, - createAbortAwareIsolatedRunner, - createDueIsolatedJob, - createIsolatedRegressionJob, - createRunningCronServiceState, - createDeferred, topOfHourOffsetMs, - writeCronStoreSnapshot, } from "../../test/helpers/cron/service-regression-fixtures.js"; import { CronService } from "./service.js"; @@ -20,16 +14,7 @@ type CronServiceOptions = ConstructorParameters[0]; export const setupCronIssueRegressionFixtures = () => setupCronRegressionFixtures({ prefix: "cron-issues-" }); -export { - createAbortAwareIsolatedRunner, - createDueIsolatedJob, - createIsolatedRegressionJob, - createRunningCronServiceState, - createDeferred, - noopLogger, - topOfHourOffsetMs, - writeCronStoreSnapshot, -}; +export { topOfHourOffsetMs }; export async function startCronForStore(params: { storePath: string; diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 6a811f997a36..a8ff43eb957e 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -46,11 +46,11 @@ import { } from "../agents/live-model-filter.js"; import { createLiveTargetMatcher } from "../agents/live-target-matcher.js"; import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "../agents/live-test-helpers.js"; +import { shouldSkipLiveProviderDrift } from "../agents/live-test-provider-drift.js"; import { isLiveBillingDrift, isLiveRateLimitDrift, - shouldSkipLiveProviderDrift, -} from "../agents/live-test-provider-drift.js"; +} from "../agents/live-test-provider-drift.test-support.js"; import { getApiKeyForModel, resolveEnvApiKey } from "../agents/model-auth.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import { shouldSuppressBuiltInModel } from "../agents/model-suppression.js"; diff --git a/src/gateway/server-http.test-harness.ts b/src/gateway/server-http.test-harness.ts index 5a30147978ce..626b555f2104 100644 --- a/src/gateway/server-http.test-harness.ts +++ b/src/gateway/server-http.test-harness.ts @@ -5,7 +5,6 @@ import { expect, vi } from "vitest"; import type { createSubsystemLogger } from "../logging/subsystem.js"; import type { ResolvedGatewayAuth } from "./auth.js"; import { createGatewayRequest, createHooksConfig } from "./hooks-test-helpers.js"; -import { canonicalizePathVariant } from "./security-path.js"; import { createGatewayHttpServer } from "./server-http.js"; import { createHooksRequestHandler } from "./server/hooks-request-handler.js"; import { withTempConfig } from "./test-temp-config.js"; @@ -199,20 +198,6 @@ export function expectUnauthorizedResponse( expect(response.getBody(), label).toContain("Unauthorized"); } -export function createCanonicalizedChannelPluginHandler() { - return vi.fn(async (req: IncomingMessage, res: ServerResponse) => { - const pathname = new URL(req.url ?? "/", "http://localhost").pathname; - const canonicalPath = canonicalizePathVariant(pathname); - if (canonicalPath !== "/api/channels/nostr/default/profile") { - return false; - } - res.statusCode = 200; - res.setHeader("Content-Type", "application/json; charset=utf-8"); - res.end(JSON.stringify({ ok: true, route: "channel-canonicalized" })); - return true; - }); -} - export function createHooksHandler( params: | string @@ -245,47 +230,6 @@ type RouteVariant = { path: string; }; -export const CANONICAL_UNAUTH_VARIANTS: RouteVariant[] = [ - { label: "case-variant", path: "/API/channels/nostr/default/profile" }, - { label: "encoded-slash", path: "/api/channels%2Fnostr%2Fdefault%2Fprofile" }, - { - label: "encoded-slash-4x", - path: "/api%2525252fchannels%2525252fnostr%2525252fdefault%2525252fprofile", - }, - { label: "encoded-segment", path: "/api/%63hannels/nostr/default/profile" }, - { label: "dot-traversal-encoded-slash", path: "/api/foo/..%2fchannels/nostr/default/profile" }, - { - label: "dot-traversal-encoded-dotdot-slash", - path: "/api/foo/%2e%2e%2fchannels/nostr/default/profile", - }, - { - label: "dot-traversal-double-encoded", - path: "/api/foo/%252e%252e%252fchannels/nostr/default/profile", - }, - { label: "duplicate-slashes", path: "/api/channels//nostr/default/profile" }, - { label: "trailing-slash", path: "/api/channels/nostr/default/profile/" }, - { label: "malformed-short-percent", path: "/api/channels%2" }, - { label: "malformed-double-slash-short-percent", path: "/api//channels%2" }, -]; - -export const CANONICAL_AUTH_VARIANTS: RouteVariant[] = [ - { label: "auth-case-variant", path: "/API/channels/nostr/default/profile" }, - { - label: "auth-encoded-slash-4x", - path: "/api%2525252fchannels%2525252fnostr%2525252fdefault%2525252fprofile", - }, - { label: "auth-encoded-segment", path: "/api/%63hannels/nostr/default/profile" }, - { label: "auth-duplicate-trailing-slash", path: "/api/channels//nostr/default/profile/" }, - { - label: "auth-dot-traversal-encoded-slash", - path: "/api/foo/..%2fchannels/nostr/default/profile", - }, - { - label: "auth-dot-traversal-double-encoded", - path: "/api/foo/%252e%252e%252fchannels/nostr/default/profile", - }, -]; - export function buildChannelPathFuzzCorpus(): RouteVariant[] { const variants = [ "/api/channels/nostr/default/profile", @@ -315,18 +259,3 @@ export async function expectUnauthorizedVariants(params: { expectUnauthorizedResponse(response, variant.label); } } - -export async function expectAuthorizedVariants(params: { - server: GatewayHttpServer; - variants: RouteVariant[]; - authorization: string; -}) { - for (const variant of params.variants) { - const response = await sendRequest(params.server, { - path: variant.path, - authorization: params.authorization, - }); - expect(response.res.statusCode, variant.label).toBe(200); - expect(response.getBody(), variant.label).toContain('"route":"channel-canonicalized"'); - } -} diff --git a/src/gateway/server-methods/agent.test-harness.ts b/src/gateway/server-methods/agent.test-harness.ts index 6b10b892d22c..a5bf089613a1 100644 --- a/src/gateway/server-methods/agent.test-harness.ts +++ b/src/gateway/server-methods/agent.test-harness.ts @@ -21,7 +21,7 @@ import { agentHandlers } from "./agent.js"; import { suspendHandlers } from "./suspend.js"; import type { GatewayRequestContext } from "./types.js"; -export const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]); +const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]); export const REAL_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", @@ -327,7 +327,7 @@ export const makeContext = (): GatewayRequestContext => getRuntimeConfig: () => mocks.loadConfigReturn, }) as unknown as GatewayRequestContext; -export type AgentHandler = NonNullable; +type AgentHandler = NonNullable; export type AgentHandlerArgs = Parameters[0]; @@ -335,21 +335,21 @@ export type AgentParams = AgentHandlerArgs["params"]; export type AgentCommandCall = Record; -export type AgentIdentityGetHandler = NonNullable<(typeof agentHandlers)["agent.identity.get"]>; +type AgentIdentityGetHandler = NonNullable<(typeof agentHandlers)["agent.identity.get"]>; -export type AgentIdentityGetHandlerArgs = Parameters[0]; +type AgentIdentityGetHandlerArgs = Parameters[0]; -export type AgentIdentityGetParams = AgentIdentityGetHandlerArgs["params"]; +type AgentIdentityGetParams = AgentIdentityGetHandlerArgs["params"]; -export const realSetTimeout = globalThis.setTimeout.bind(globalThis); +const realSetTimeout = globalThis.setTimeout.bind(globalThis); -export let dateOnlyFakeClockActive = false; +let dateOnlyFakeClockActive = false; export function setDateOnlyFakeClockActive(active: boolean): void { dateOnlyFakeClockActive = active; } -export function waitForRealTimer(ms: number) { +function waitForRealTimer(ms: number) { return new Promise((resolve) => { realSetTimeout(resolve, ms); }); @@ -439,7 +439,7 @@ export async function flushScheduledDispatchStep() { await Promise.resolve(); } -export async function waitForAcceptedRunDispatch(params: { +async function waitForAcceptedRunDispatch(params: { respond: ReturnType; commandCallCount: number; }) { @@ -486,20 +486,20 @@ export function buildExistingMainStoreEntry(overrides: Record = }; } -export type SessionStoreFixture = Record>; +type SessionStoreFixture = Record>; -export type SessionEntryTargetFixture = { +type SessionEntryTargetFixture = { canonicalKey: string; storeKeys: string[]; }; -export function cloneSessionStoreFixtureEntry( +function cloneSessionStoreFixtureEntry( entry: Record | undefined, ): Record | undefined { return entry ? structuredClone(entry) : undefined; } -export function selectFreshestTargetFixtureEntry( +function selectFreshestTargetFixtureEntry( store: SessionStoreFixture, target: SessionEntryTargetFixture, ): { entry: Record; key: string } | undefined { @@ -520,7 +520,7 @@ export function selectFreshestTargetFixtureEntry( return freshest; } -export function resetSessionAccessorMocks() { +function resetSessionAccessorMocks() { mocks.readTranscriptStatsSync.mockReset().mockReturnValue({ eventCount: 0, maxSeq: 0, @@ -688,7 +688,7 @@ export async function runMainAgentAndCaptureEntry(idempotencyKey: string) { return requireValue(capturedEntry, "updated session entry missing"); } -export function readLastAgentCommandCall(): AgentCommandCall | undefined { +function readLastAgentCommandCall(): AgentCommandCall | undefined { const calls = mocks.agentCommand.mock.calls; const call = calls[calls.length - 1]; return call?.[0] as AgentCommandCall | undefined; @@ -924,7 +924,7 @@ export async function invokeAgentIdentityGet( return respond; } -export function toLintErrorObject(value: unknown, fallbackMessage: string): Error { +function toLintErrorObject(value: unknown, fallbackMessage: string): Error { if (value instanceof Error) { return value; } @@ -969,7 +969,7 @@ export const describe0AfterEach0 = () => { vi.useRealTimers(); }; -export function resetIntegrationState() { +function resetIntegrationState() { envSnapshot.restore(); resetDetachedTaskLifecycleRuntimeForTests(); resetTaskRegistryForTests(); diff --git a/src/gateway/server-methods/chat.test-helpers.ts b/src/gateway/server-methods/chat.test-helpers.ts deleted file mode 100644 index 582672ad1d33..000000000000 --- a/src/gateway/server-methods/chat.test-helpers.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Fixtures for chat method tests that need a real persisted session transcript. - */ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "../../config/sessions/version.js"; - -/** Writes a minimal current-version transcript file and returns its temp location. */ -export function createTranscriptFixtureSync(params: { - prefix: string; - sessionId: string; - fileName?: string; -}) { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), params.prefix)); - const transcriptPath = path.join(dir, params.fileName ?? "sess.jsonl"); - fs.writeFileSync( - transcriptPath, - `${JSON.stringify({ - type: "session", - version: CURRENT_SESSION_VERSION, - id: params.sessionId, - timestamp: new Date(0).toISOString(), - cwd: "/tmp", - })}\n`, - "utf-8", - ); - return { dir, transcriptPath }; -} diff --git a/src/gateway/server.agent.gateway-server-agent.mocks.ts b/src/gateway/server.agent.gateway-server-agent.mocks.ts index d193a5027ce0..7ebfec1cc2fa 100644 --- a/src/gateway/server.agent.gateway-server-agent.mocks.ts +++ b/src/gateway/server.agent.gateway-server-agent.mocks.ts @@ -6,7 +6,7 @@ import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/regis import { setActivePluginRegistry as setActivePluginRegistryLocal } from "../plugins/runtime.js"; import { setTestPluginRegistry } from "./test-helpers.plugin-registry.js"; -export const registryState: { registry: PluginRegistry } = { +const registryState: { registry: PluginRegistry } = { registry: createEmptyPluginRegistry(), }; diff --git a/src/gateway/server.auth.test-helpers.ts b/src/gateway/server.auth.test-helpers.ts index 20aea15d308a..5a0dfcd4ebe2 100644 --- a/src/gateway/server.auth.test-helpers.ts +++ b/src/gateway/server.auth.test-helpers.ts @@ -12,7 +12,6 @@ import { withEnvAsync } from "../test-utils/env.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { buildDeviceAuthPayload } from "./device-auth.js"; import { - createGatewaySuiteHarness, connectReq, getTrackedConnectChallengeNonce, getFreePort, @@ -391,18 +390,14 @@ async function ensurePairedDeviceTokenForCurrentIdentity(ws: WebSocket): Promise } export { - approvePendingPairingIfNeeded, BACKEND_GATEWAY_CLIENT, - buildDeviceAuthPayload, configureTrustedProxyControlUiAuth, connectReq, CONTROL_UI_CLIENT, createSignedDevice, - createGatewaySuiteHarness, ensurePairedDeviceTokenForCurrentIdentity, expectHelloOkServerVersion, getFreePort, - getTrackedConnectChallengeNonce, installGatewayTestHooks, MIN_PROBE_PROTOCOL_VERSION, NODE_CLIENT, @@ -420,7 +415,6 @@ export { startServer, startServerWithClient, TEST_OPERATOR_CLIENT, - trackConnectChallengeNonce, TRUSTED_PROXY_CONTROL_UI_HEADERS, testState, testTailscaleWhois, diff --git a/src/gateway/server.e2e-ws-harness.ts b/src/gateway/server.e2e-ws-harness.ts index 2fbf1991b694..db7a1edf01bc 100644 --- a/src/gateway/server.e2e-ws-harness.ts +++ b/src/gateway/server.e2e-ws-harness.ts @@ -9,7 +9,7 @@ import { trackConnectChallengeNonce, } from "./test-helpers.js"; -export type GatewayWsClient = { +type GatewayWsClient = { ws: WebSocket; hello: unknown; }; diff --git a/src/gateway/test-helpers.lan-pairing.ts b/src/gateway/test-helpers.lan-pairing.ts index c28b79c377b3..285a54af5033 100644 --- a/src/gateway/test-helpers.lan-pairing.ts +++ b/src/gateway/test-helpers.lan-pairing.ts @@ -10,7 +10,7 @@ import { loadDeviceIdentity } from "./device-authz.test-helpers.js"; import { pickPrimaryLanIPv4 } from "./net.js"; import { connectReq, startServer, trackConnectChallengeNonce } from "./test-helpers.js"; -export const LAN_NODE_PAIRING_TOKEN = "secret"; +const LAN_NODE_PAIRING_TOKEN = "secret"; const NODE_CLIENT = { id: GATEWAY_CLIENT_NAMES.NODE_HOST, @@ -81,7 +81,7 @@ async function canUseLanSelfConnect(host: string): Promise { }); } -export type LanNodePairingContext = { +type LanNodePairingContext = { lanIp: string; loaded: ReturnType; /** Open a fresh LAN WebSocket, run the node connect handshake, close it. */ diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index b9fe5716f36b..4abb48247c70 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -2,51 +2,20 @@ // Centralizes Vitest mock wiring for agent, channel, plugin, and runtime seams. import path from "node:path"; import { vi } from "vitest"; -import { - getTestPluginRegistry, - resetTestPluginRegistry, - setTestPluginRegistry, -} from "./test-helpers.plugin-registry.js"; +import { getTestPluginRegistry } from "./test-helpers.plugin-registry.js"; import { agentCommand, cronIsolatedRun, - dispatchInboundMessageMock, embeddedRunMock, type GetReplyFromConfigFn, - getReplyFromConfig, getGatewayTestHoistedState, - mockGetReplyFromConfigOnce, agentDiscoveryMock, - runBtwSideQuestion, - sendWhatsAppMock, sessionStoreSaveDelayMs, - setTestConfigRoot, - testIsNixMode, - testState, testTailnetIPv4, testTailscaleWhois, type RunBtwSideQuestionFn, } from "./test-helpers.runtime-state.js"; -export { getTestPluginRegistry, resetTestPluginRegistry, setTestPluginRegistry }; -export { - agentCommand, - cronIsolatedRun, - dispatchInboundMessageMock, - embeddedRunMock, - getReplyFromConfig, - mockGetReplyFromConfigOnce, - agentDiscoveryMock, - runBtwSideQuestion, - sendWhatsAppMock, - sessionStoreSaveDelayMs, - setTestConfigRoot, - testIsNixMode, - testState, - testTailnetIPv4, - testTailscaleWhois, -}; - const gatewayTestHoisted = getGatewayTestHoistedState(); function createEmbeddedRunMockExports() { diff --git a/src/gateway/test-helpers.runtime-state.ts b/src/gateway/test-helpers.runtime-state.ts index 530c1095175a..50a68aba3035 100644 --- a/src/gateway/test-helpers.runtime-state.ts +++ b/src/gateway/test-helpers.runtime-state.ts @@ -155,7 +155,6 @@ export const testTailscaleWhois = gatewayTestHoisted.testTailscaleWhois; export const agentDiscoveryMock = gatewayTestHoisted.agentDiscoveryMock; export const cronIsolatedRun = gatewayTestHoisted.cronIsolatedRun; export const agentCommand = gatewayTestHoisted.agentCommand; -export const runBtwSideQuestion = gatewayTestHoisted.runBtwSideQuestion; export const dispatchInboundMessageMock = gatewayTestHoisted.dispatchInboundMessage; export const getReplyFromConfig = gatewayTestHoisted.getReplyFromConfig; export const mockGetReplyFromConfigOnce = (impl: GetReplyFromConfigFn) => { diff --git a/src/gateway/test-helpers.ts b/src/gateway/test-helpers.ts index d85bff30894e..f7ef3f745f49 100644 --- a/src/gateway/test-helpers.ts +++ b/src/gateway/test-helpers.ts @@ -10,7 +10,6 @@ export { mockGetReplyFromConfigOnce, agentDiscoveryMock, testState, - testTailnetIPv4, testTailscaleWhois, } from "./test-helpers.runtime-state.js"; export { resetTestPluginRegistry, setTestPluginRegistry } from "./test-helpers.plugin-registry.js"; diff --git a/src/gateway/test/server-sessions.test-helpers.ts b/src/gateway/test/server-sessions.test-helpers.ts index 1e82e70e839b..411a8bb35bef 100644 --- a/src/gateway/test/server-sessions.test-helpers.ts +++ b/src/gateway/test/server-sessions.test-helpers.ts @@ -39,28 +39,6 @@ export async function getSessionsHandlers() { return (await import("../server-methods/sessions.js")).sessionsHandlers; } -export function createLinearSessionTranscript(sessionId: string, contents: string[]): string { - const records: Array> = [ - { - type: "session", - version: 3, - id: sessionId, - timestamp: "2026-06-19T12:00:00.000Z", - cwd: "/tmp", - }, - ]; - for (const [index, content] of contents.entries()) { - records.push({ - type: "message", - id: `${sessionId}-entry-${index}`, - parentId: index === 0 ? null : `${sessionId}-entry-${index - 1}`, - timestamp: `2026-06-19T12:00:${String(index + 1).padStart(2, "0")}.000Z`, - message: { role: "user", content, timestamp: index + 1 }, - }); - } - return `${records.map((record) => JSON.stringify(record)).join("\n")}\n`; -} - type TestTranscriptMessage = Record & { role: string; }; diff --git a/src/gateway/worker-environments/placement-dispatch-test-fixtures.ts b/src/gateway/worker-environments/placement-dispatch-test-fixtures.ts index 342be5e36b23..8a32cc453e28 100644 --- a/src/gateway/worker-environments/placement-dispatch-test-fixtures.ts +++ b/src/gateway/worker-environments/placement-dispatch-test-fixtures.ts @@ -12,9 +12,7 @@ type WorkerDispatchRequest = Parameters< ReturnType["dispatch"] >[0]; export type PlacementStore = ReturnType; -export type DispatchEnvironmentRecord = Awaited< - ReturnType ->; +type DispatchEnvironmentRecord = Awaited>; export type DispatchStage = | "barrier" | "workspace" diff --git a/src/infra/outbound/targets.test-helpers.ts b/src/infra/outbound/targets.test-helpers.ts index 5910d327036b..5936c08dbb51 100644 --- a/src/infra/outbound/targets.test-helpers.ts +++ b/src/infra/outbound/targets.test-helpers.ts @@ -21,7 +21,7 @@ function stripTestPrefix(raw: string, channelId: string): string { } /** Parses forum test targets with optional topic/thread suffixes. */ -export function parseForumTargetForTest(raw: string): { +function parseForumTargetForTest(raw: string): { roomId: string; threadId?: number; chatType: "direct" | "group" | "unknown"; diff --git a/src/infra/sqlite-pragma.test-support.ts b/src/infra/sqlite-pragma.test-support.ts index d8334daecc5d..5ef6bff538e9 100644 --- a/src/infra/sqlite-pragma.test-support.ts +++ b/src/infra/sqlite-pragma.test-support.ts @@ -2,7 +2,7 @@ import type { DatabaseSync } from "node:sqlite"; // SQLite pragma test helpers normalize node:sqlite bigint/number outputs. -export type SqliteNumberPragma = +type SqliteNumberPragma = | "auto_vacuum" | "busy_timeout" | "foreign_keys" diff --git a/src/plugin-sdk/test-helpers/provider-auth-contract.ts b/src/plugin-sdk/test-helpers/provider-auth-contract.ts index 2a1b4e6d525b..f5bb0b1d59c7 100644 --- a/src/plugin-sdk/test-helpers/provider-auth-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-auth-contract.ts @@ -28,7 +28,7 @@ export type ProviderAuthContractPluginLoader = () => Promise<{ default: Parameters[0]; }>; -export type OpenAICodexProviderAuthContractOptions = { +type OpenAICodexProviderAuthContractOptions = { expectedCodexDefaultModel: string; loginOpenAICodexOAuthMock: ReturnType>; }; diff --git a/src/plugins/hooks.test-fixtures.ts b/src/plugins/hooks.test-fixtures.ts index c5ae1de404c2..5da63e63cf47 100644 --- a/src/plugins/hooks.test-fixtures.ts +++ b/src/plugins/hooks.test-fixtures.ts @@ -5,10 +5,7 @@ import type { PluginRegistry } from "./registry.js"; import type { PluginHookAgentContext, PluginHookRegistration } from "./types.js"; export { addTestHook, createMockPluginRegistry }; -export type { - PluginHookBeforeDispatchResult, - PluginHookReplyDispatchResult, -} from "./hook-types.js"; +export type { PluginHookReplyDispatchResult } from "./hook-types.js"; export type PluginTargetedInboundClaimOutcome = Awaited< ReturnType["runInboundClaimForPluginOutcome"]> >; diff --git a/src/plugins/loader.test-harness.ts b/src/plugins/loader.test-harness.ts index e6666be5e365..cf7ef0498f25 100644 --- a/src/plugins/loader.test-harness.ts +++ b/src/plugins/loader.test-harness.ts @@ -34,11 +34,11 @@ export const listEmbeddingProviders = () => export let cachedBundledTelegramDir = ""; -export let cachedBundledMemoryDir = ""; +let cachedBundledMemoryDir = ""; -export type GlobalHookRunner = NonNullable>; +type GlobalHookRunner = NonNullable>; -export type PluginStartupTraceDetail = { +type PluginStartupTraceDetail = { name: string; metrics: ReadonlyArray; }; @@ -87,7 +87,7 @@ export function createDetachedTaskRuntimeStub(id: string): DetachedTaskLifecycle }; } -export const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = { +const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = { id: "telegram", register(api) { api.registerChannel({ @@ -597,7 +597,7 @@ export function createErrorLogger(errors: string[]) { }; } -export function createEscapingEntryFixture(params: { id: string; sourceBody: string }) { +function createEscapingEntryFixture(params: { id: string; sourceBody: string }) { const pluginDir = makeTempDir(); const outsideDir = makeTempDir(); const outsideEntry = path.join(outsideDir, "outside.cjs"); @@ -618,7 +618,7 @@ export function createEscapingEntryFixture(params: { id: string; sourceBody: str return { pluginDir, outsideEntry, linkedEntry }; } -export function resolveLoadedPluginSource( +function resolveLoadedPluginSource( registry: ReturnType, pluginId: string, ) { diff --git a/src/plugins/memory-state.test-fixtures.ts b/src/plugins/memory-state.test-fixtures.ts index 2c53ea4730e1..b8b1e89839c3 100644 --- a/src/plugins/memory-state.test-fixtures.ts +++ b/src/plugins/memory-state.test-fixtures.ts @@ -1,10 +1,6 @@ /** Test-only compatibility fixtures for plugin memory state. */ import { - registerMemoryFlushPlanResolverForPlugin, registerMemoryPromptSectionForPlugin, - registerMemoryRuntimeForPlugin, - type MemoryFlushPlanResolver, - type MemoryPluginRuntime, type MemoryPromptSectionBuilder, } from "./memory-state.js"; @@ -13,11 +9,3 @@ export * from "./memory-state.js"; export function registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void { registerMemoryPromptSectionForPlugin("test-memory", builder); } - -export function registerMemoryFlushPlanResolver(resolver: MemoryFlushPlanResolver): void { - registerMemoryFlushPlanResolverForPlugin("test-memory", resolver); -} - -export function registerMemoryRuntime(runtime: MemoryPluginRuntime): void { - registerMemoryRuntimeForPlugin("test-memory", runtime); -} diff --git a/src/process/exec-spawn.ts b/src/process/exec-spawn.ts index b20fc4cabe8c..d9c373c86489 100644 --- a/src/process/exec-spawn.ts +++ b/src/process/exec-spawn.ts @@ -55,7 +55,7 @@ export function shouldSpawnWithShell(params: { return false; } -export type SpawnCommandOptions = Omit< +type SpawnCommandOptions = Omit< ExecaOptions, "env" | "extendEnv" | "shell" | "windowsHide" | "windowsVerbatimArguments" > & { diff --git a/src/process/exec.ts b/src/process/exec.ts index 4342b31ad360..e32fab7b42ca 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -15,7 +15,6 @@ export type { CommandOptions } from "./exec-runner.js"; export { isPlainCommandExitFailure, resolveProcessExitCode } from "./exec-result.js"; export type { SpawnResult } from "./exec-result.js"; export { resolveCommandEnv, shouldSpawnWithShell, spawnCommand } from "./exec-spawn.js"; -export type { SpawnCommandOptions } from "./exec-spawn.js"; const DEFAULT_EXEC_MAX_BUFFER_BYTES = 1024 * 1024; diff --git a/src/secrets/runtime.integration.test-helpers.ts b/src/secrets/runtime.integration.test-helpers.ts index 491c4976f9df..35d7601d389a 100644 --- a/src/secrets/runtime.integration.test-helpers.ts +++ b/src/secrets/runtime.integration.test-helpers.ts @@ -8,13 +8,8 @@ import type { SecretsRuntimeEnvSnapshot } from "./runtime-openai-file-fixture.te /** Shared integration helpers for full secrets runtime snapshot tests. */ export { asConfig, - createOpenAIFileRuntimeConfig, - createOpenAIFileRuntimeFixture, EMPTY_LOADABLE_PLUGIN_ORIGINS, - expectResolvedOpenAIRuntime, loadAuthStoreWithProfiles, - OPENAI_ENV_KEY_REF, - OPENAI_FILE_KEY_REF, } from "./runtime-openai-file-fixture.test-helper.js"; export type { SecretsRuntimeEnvSnapshot } from "./runtime-openai-file-fixture.test-helper.js"; import { clearSecretsRuntimeSnapshot } from "./runtime.js"; diff --git a/src/skills/loading/workspace.ts b/src/skills/loading/workspace.ts index 726c0c86f91c..ac9f5b103ddb 100644 --- a/src/skills/loading/workspace.ts +++ b/src/skills/loading/workspace.ts @@ -1802,5 +1802,4 @@ export function filterWorkspaceSkillEntriesWithOptions( ): SkillEntry[] { return filterSkillEntries(entries, opts?.config, opts?.skillFilter, opts?.eligibility); } -export { testing as __testing }; /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/src/skills/security/scanner.ts b/src/skills/security/scanner.ts index b0dc285bb02c..1c3802a22f91 100644 --- a/src/skills/security/scanner.ts +++ b/src/skills/security/scanner.ts @@ -11,7 +11,7 @@ import { formatScanEvidence, LITERAL_SECRET_SKILL_CONTENT_RULE } from "./scan-ev // Types // --------------------------------------------------------------------------- -export type SkillScanSeverity = "info" | "warn" | "critical"; +type SkillScanSeverity = "info" | "warn" | "critical"; export type SkillScanFinding = { ruleId: string; @@ -22,7 +22,7 @@ export type SkillScanFinding = { evidence: string; }; -export type SkillScanSummary = { +type SkillScanSummary = { scannedFiles: number; critical: number; warn: number; diff --git a/src/state/sqlite-schema-shape.test-support.ts b/src/state/sqlite-schema-shape.test-support.ts index 6c353c1afc2b..684c5aa6e4ad 100644 --- a/src/state/sqlite-schema-shape.test-support.ts +++ b/src/state/sqlite-schema-shape.test-support.ts @@ -33,7 +33,7 @@ type IndexTermShape = { }; /** Comparable SQLite schema summary used by generated-schema tests. */ -export type SqliteSchemaShape = Record< +type SqliteSchemaShape = Record< string, { columns: ColumnShape[]; diff --git a/src/system-agent/system-agent.test-helpers.ts b/src/system-agent/system-agent.test-helpers.ts index 20696ca2e0d9..051623ff6408 100644 --- a/src/system-agent/system-agent.test-helpers.ts +++ b/src/system-agent/system-agent.test-helpers.ts @@ -15,7 +15,7 @@ import { type SystemAgentVerifiedInferenceDeps, } from "./verified-inference.js"; -export type SystemAgentVerifiedInferenceTestFixture = { +type SystemAgentVerifiedInferenceTestFixture = { binding: SystemAgentVerifiedInferenceBinding; deps: SystemAgentVerifiedInferenceDeps; }; diff --git a/src/test-utils/bundled-plugin-public-surface.ts b/src/test-utils/bundled-plugin-public-surface.ts index 48ce1967a3ca..8f997dad6390 100644 --- a/src/test-utils/bundled-plugin-public-surface.ts +++ b/src/test-utils/bundled-plugin-public-surface.ts @@ -1,18 +1,11 @@ // Test helper for asserting bundled plugin public surface files. import fs from "node:fs"; import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; import { uniqueStrings } from "@openclaw/normalization-core/string-normalization"; -import { - loadBundledPluginPublicSurfaceModule, - loadBundledPluginPublicSurfaceModuleSync, -} from "../plugin-sdk/facade-loader.js"; +import { loadBundledPluginPublicSurfaceModule } from "../plugin-sdk/facade-loader.js"; import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js"; import { findBundledPluginMetadataById } from "../plugins/bundled-plugin-metadata.js"; -import { - getCachedPluginSourceModuleLoader, - type PluginModuleLoaderCache, -} from "../plugins/plugin-module-loader-cache.js"; import { normalizeBundledPluginArtifactSubpath } from "../plugins/public-surface-runtime.js"; import { resolveLoaderPackageRoot } from "../plugins/sdk-alias.js"; @@ -26,8 +19,6 @@ type BundledPluginPublicSurfaceMetadata = Pick< NonNullable>, "dirName" >; -const sourceModuleLoaders: PluginModuleLoaderCache = new Map(); - function isSafeBundledPluginDirName(pluginId: string): boolean { return /^[a-z0-9][a-z0-9._-]*$/u.test(pluginId); } @@ -74,84 +65,11 @@ function findBundledPluginMetadata(pluginId: string): BundledPluginPublicSurface return metadata; } -function readPackageName(packageDir: string): string | undefined { - try { - const packageJsonPath = path.join(packageDir, "package.json"); - const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown }; - return typeof parsed.name === "string" ? parsed.name : undefined; - } catch { - return undefined; - } -} - -function resolveWorkspacePackageDir(packageName: string): string { - const rawRoots = [ - resolveBundledPluginsDir(), - path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions"), - path.resolve(OPENCLAW_PACKAGE_ROOT, "dist-runtime", "extensions"), - path.resolve(OPENCLAW_PACKAGE_ROOT, "dist", "extensions"), - ].filter((entry): entry is string => Boolean(entry)); - const roots = uniqueStrings(rawRoots); - - for (const root of roots) { - let entries: string[]; - try { - entries = fs.readdirSync(root); - } catch { - continue; - } - for (const entry of entries) { - const packageDir = path.join(root, entry); - if (readPackageName(packageDir) === packageName) { - return packageDir; - } - } - } - throw new Error(`Unknown workspace package: ${packageName}`); -} - -// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Test loaders use caller-supplied module surface types. -type BundledPluginPublicSurfaceLoader = (params: { - pluginId: string; - artifactBasename: string; -}) => T; - type AsyncBundledPluginPublicSurfaceLoader = (params: { pluginId: string; artifactBasename: string; }) => Promise; -// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Test loaders use caller-supplied module surface types. -type BundledPluginPublicArtifactLoader = (pluginId: string) => T; - -export const loadBundledPluginPublicSurfaceSync: BundledPluginPublicSurfaceLoader = (params) => { - const metadata = findBundledPluginMetadata(params.pluginId); - return loadBundledPluginPublicSurfaceModuleSync({ - dirName: metadata.dirName, - artifactBasename: normalizeBundledPluginArtifactSubpath(params.artifactBasename), - }); -}; - -export function loadBundledPluginPublicSurfaceSourceSync(params: { - pluginId: string; - artifactBasename: string; -}): object { - const modulePath = resolveVitestSourceModulePath( - resolveBundledPluginPublicModulePath({ - pluginId: params.pluginId, - artifactBasename: params.artifactBasename, - }), - ); - const loader = getCachedPluginSourceModuleLoader({ - cache: sourceModuleLoaders, - modulePath, - importerUrl: import.meta.url, - loaderFilename: import.meta.url, - pluginSdkResolution: "src", - }); - return loader(modulePath) as object; -} - export const loadBundledPluginPublicSurface: AsyncBundledPluginPublicSurfaceLoader = (params) => { const metadata = findBundledPluginMetadata(params.pluginId); return loadBundledPluginPublicSurfaceModule({ @@ -160,34 +78,6 @@ export const loadBundledPluginPublicSurface: AsyncBundledPluginPublicSurfaceLoad }); }; -export const loadBundledPluginApiSync: BundledPluginPublicArtifactLoader = (pluginId) => { - return loadBundledPluginPublicSurfaceSync({ - pluginId, - artifactBasename: "api.js", - }); -}; - -export const loadBundledPluginContractApiSync: BundledPluginPublicArtifactLoader = (pluginId) => { - return loadBundledPluginPublicSurfaceSync({ - pluginId, - artifactBasename: "contract-api.js", - }); -}; - -export const loadBundledPluginRuntimeApiSync: BundledPluginPublicArtifactLoader = (pluginId) => { - return loadBundledPluginPublicSurfaceSync({ - pluginId, - artifactBasename: "runtime-api.js", - }); -}; - -export const loadBundledPluginTestApiSync: BundledPluginPublicArtifactLoader = (pluginId) => { - return loadBundledPluginPublicSurfaceSync({ - pluginId, - artifactBasename: "test-api.js", - }); -}; - export function resolveBundledPluginPublicModulePath(params: { pluginId: string; artifactBasename: string; @@ -234,16 +124,3 @@ export function resolveRelativeBundledPluginPublicModuleId(params: { .replaceAll(path.sep, "/"); return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; } - -export function resolveWorkspacePackagePublicModuleUrl(params: { - packageName: string; - artifactBasename: string; -}): string { - const targetPath = resolveVitestSourceModulePath( - path.resolve( - resolveWorkspacePackageDir(params.packageName), - normalizeBundledPluginArtifactSubpath(params.artifactBasename), - ), - ); - return pathToFileURL(targetPath).href; -} diff --git a/src/test-utils/deferred.ts b/src/test-utils/deferred.ts index b43a40d7f345..17c416919c08 100644 --- a/src/test-utils/deferred.ts +++ b/src/test-utils/deferred.ts @@ -1 +1 @@ -export { createDeferred, type Deferred } from "../shared/deferred.js"; +export { createDeferred } from "../shared/deferred.js"; diff --git a/src/test-utils/mock-http.ts b/src/test-utils/mock-http.ts index ebc674902100..a3a71acc5f75 100644 --- a/src/test-utils/mock-http.ts +++ b/src/test-utils/mock-http.ts @@ -14,7 +14,7 @@ type MockHttpHeaderMatcher = type MockHttpHeaders = Record; type MockHttpBody = string | Buffer | Uint8Array | ArrayBuffer; -export type MockHttpReply = +type MockHttpReply = | { status?: number; body?: MockHttpBody; @@ -28,7 +28,7 @@ export type MockHttpReply = headers?: MockHttpHeaders; }; -export type MockHttpInterceptor = { +type MockHttpInterceptor = { url: string | URL; method?: string; requestBody?: MockHttpValueMatcher; @@ -37,7 +37,7 @@ export type MockHttpInterceptor = { times?: number; }; -export type MockHttp = { +type MockHttp = { setup: () => void; intercept: (params: MockHttpInterceptor) => void; requests: () => MockCallHistoryLog[]; diff --git a/src/test-utils/openclaw-test-state.ts b/src/test-utils/openclaw-test-state.ts index 9899acd0d710..d858f65ec1f8 100644 --- a/src/test-utils/openclaw-test-state.ts +++ b/src/test-utils/openclaw-test-state.ts @@ -24,7 +24,7 @@ type OpenClawTestStateScenario = | "gateway-loopback" | "external-service"; -export type OpenClawTestStateOptions = { +type OpenClawTestStateOptions = { prefix?: string; label?: string; layout?: OpenClawTestStateLayout; diff --git a/src/test-utils/process-tree.ts b/src/test-utils/process-tree.ts index b5ad49d87946..be4c564cf001 100644 --- a/src/test-utils/process-tree.ts +++ b/src/test-utils/process-tree.ts @@ -22,7 +22,7 @@ export async function writeForkingNoOutputScript(dir: string): Promise { return scriptPath; } -export function isPidAlive(pid: number): boolean { +function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; diff --git a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts index 14a3757b83bc..3bcdf3e7c237 100644 --- a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts +++ b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts @@ -1,7 +1,7 @@ // Shared QA Lab fixture for dispatching existing Docker E2E lanes. import { spawnSync } from "node:child_process"; -export type QaDockerE2eLaneDefinition = { +type QaDockerE2eLaneDefinition = { env?: (env: NodeJS.ProcessEnv) => Record; script: string; }; @@ -18,7 +18,7 @@ type SpawnQaDockerE2eLane = ( options: { env: NodeJS.ProcessEnv; stdio: "inherit" }, ) => QaDockerE2eLaneRunResult; -export const QA_DOCKER_E2E_LANES = { +const QA_DOCKER_E2E_LANES = { "agent-bundle-mcp-tools": { script: "scripts/e2e/agent-bundle-mcp-tools-docker.sh", }, @@ -87,14 +87,11 @@ export const QA_DOCKER_E2E_LANES = { }, } satisfies Record; -export type QaDockerE2eLaneName = keyof typeof QA_DOCKER_E2E_LANES; +type QaDockerE2eLaneName = keyof typeof QA_DOCKER_E2E_LANES; -export type QaDockerE2eLaneArgs = - | { kind: "help" } - | { kind: "list" } - | { kind: "run"; laneName: string }; +type QaDockerE2eLaneArgs = { kind: "help" } | { kind: "list" } | { kind: "run"; laneName: string }; -export type ResolvedQaDockerE2eLane = { +type ResolvedQaDockerE2eLane = { env: NodeJS.ProcessEnv; name: QaDockerE2eLaneName; script: string; diff --git a/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts b/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts index 2251d5d37708..8fa5c234d3a6 100644 --- a/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts +++ b/test/e2e/qa-lab/runtime/heartbeat-active-hours-runtime.ts @@ -154,8 +154,6 @@ export async function runHeartbeatActiveHoursRuntime(options: HeartbeatRuntimeOp } } -export const testing = { heartbeatConfig, parseOptions, waitForObservation }; - if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { runHeartbeatActiveHoursRuntime(parseOptions(process.argv.slice(2))) .then((evidence) => { diff --git a/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts b/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts index 705e2813d8e4..20e737d030fd 100644 --- a/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts +++ b/test/e2e/qa-lab/runtime/mcp-channels.fixture.ts @@ -21,10 +21,7 @@ const [{ PROTOCOL_VERSION }, { formatErrorMessage }, { readStringValue }] = awai ]); import { resolveGatewaySuccessPayload } from "../../../../scripts/e2e/lib/gateway-frame-payload.mjs"; import { readMcpChannelLimits } from "../../../../scripts/e2e/mcp-channel-limits.ts"; -import { - createGatewayWsClient, - type GatewayEventFrame, -} from "../../../../scripts/lib/gateway-ws-client.ts"; +import { createGatewayWsClient } from "../../../../scripts/lib/gateway-ws-client.ts"; import { connectMcpWithTimeout, createMcpClientTempState, @@ -70,6 +67,10 @@ type GatewayClientInfo = { mode: string; }; +type GatewayEventFrame = Parameters< + NonNullable[0]["onEvent"]> +>[0]; + export type McpClientHandle = { client: Client; cleanup(): void; diff --git a/test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts b/test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts index 65fa1700f9ed..c8804a445f15 100644 --- a/test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts +++ b/test/e2e/qa-lab/runtime/mcp-client-temp-state.fixture.ts @@ -10,7 +10,7 @@ export type McpClientTempState = { tokenFile: string; }; -export type ReconnectableMcpClientHandle = { +type ReconnectableMcpClientHandle = { cleanup: () => void; client: { close: () => Promise }; transport: { close: () => Promise }; diff --git a/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts b/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts index 5a570ea5b6c5..c5b7ded148a2 100644 --- a/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts +++ b/test/e2e/qa-lab/runtime/openai-image-auth-docker-client.ts @@ -168,7 +168,7 @@ function createCodexOAuthStore() { } as const; } -export async function main() { +async function main() { assert( process.env.OPENAI_API_KEY === DIRECT_TOKEN, "Docker lane must expose the direct OpenAI API key", diff --git a/test/e2e/qa-lab/runtime/script-evidence.ts b/test/e2e/qa-lab/runtime/script-evidence.ts index fcf83d64e4be..d35f64ed83b3 100644 --- a/test/e2e/qa-lab/runtime/script-evidence.ts +++ b/test/e2e/qa-lab/runtime/script-evidence.ts @@ -14,7 +14,7 @@ import { redactToolPayloadTextWithConfig } from "../../../../src/logging/redact. import { withFullContextToolPayloadRedaction } from "../../../../src/logging/redact.test-support.js"; import { DEFAULT_CHILD_OUTPUT_TAIL_BYTES } from "../../../helpers/bounded-child-output.js"; -export const DEFAULT_QA_SCRIPT_EVIDENCE_DETAILS_BYTES = 32 * 1024; +const DEFAULT_QA_SCRIPT_EVIDENCE_DETAILS_BYTES = 32 * 1024; const QA_SCRIPT_STATUS_MATCH_CARRY_CHARS = 1024; const QA_SCRIPT_LOG_OVERFLOW_MESSAGE = "QA evidence log omitted: safe redaction buffer exceeded.\n"; const QA_SCRIPT_DETAILS_OVERFLOW_MESSAGE = diff --git a/test/fixtures/child-process-bridge/child.js b/test/fixtures/child-process-bridge/child.js deleted file mode 100644 index 57c7d703e38b..000000000000 --- a/test/fixtures/child-process-bridge/child.js +++ /dev/null @@ -1,11 +0,0 @@ -process.stdout.write("ready\n"); - -const keepAlive = setInterval(() => {}, 1000); - -const shutdown = () => { - clearInterval(keepAlive); - process.exit(0); -}; - -process.on("SIGTERM", shutdown); -process.on("SIGINT", shutdown); diff --git a/test/global-setup.ts b/test/global-setup.ts deleted file mode 100644 index 0f45b8fc89b9..000000000000 --- a/test/global-setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Global test setup installs shared environment before Vitest projects run. -import { installTestEnv } from "./test-env.js"; - -export default async () => { - const { cleanup } = installTestEnv(); - return () => cleanup(); -}; diff --git a/test/helpers/acp-manager-task-state.ts b/test/helpers/acp-manager-task-state.ts index 9721b3a79776..423d9300000d 100644 --- a/test/helpers/acp-manager-task-state.ts +++ b/test/helpers/acp-manager-task-state.ts @@ -11,8 +11,6 @@ import { installInMemoryTaskRegistryRuntime } from "../../src/test-utils/task-re // Shared ACP manager task registry setup for tests. -export { findTaskByRunId }; - /** Reset task and task-flow registries without persisting state. */ export function resetAcpManagerTaskStateForTests(): void { resetTaskRegistryForTests({ persist: false }); diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index 59f511cd83cf..a21b2ac4656d 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -31,7 +31,7 @@ import { // Builds Codex happy-path prompt snapshot fixtures for agent prompt regression tests. -export { CODEX_MODEL_PROMPT_FIXTURE_DIR, CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR }; +export { CODEX_RUNTIME_HAPPY_PATH_PROMPT_SNAPSHOT_DIR }; const WORKSPACE_DIR = "/tmp/openclaw-happy-path/workspace"; const AGENT_DIR = "/tmp/openclaw-happy-path/agent"; diff --git a/test/helpers/agents/prompt-composition-scenarios.ts b/test/helpers/agents/prompt-composition-scenarios.ts index e4e966e896b9..af9b1c35d3a7 100644 --- a/test/helpers/agents/prompt-composition-scenarios.ts +++ b/test/helpers/agents/prompt-composition-scenarios.ts @@ -30,7 +30,7 @@ import { makeTempWorkspace, writeWorkspaceFile } from "../../../src/test-helpers // Prompt composition scenarios for system/body prompt stability tests. /** One turn in a prompt composition scenario. */ -export type PromptScenarioTurn = { +type PromptScenarioTurn = { id: string; label: string; systemPrompt: string; diff --git a/test/helpers/auth-wizard.ts b/test/helpers/auth-wizard.ts index d226c95a500d..004c6bd37a06 100644 --- a/test/helpers/auth-wizard.ts +++ b/test/helpers/auth-wizard.ts @@ -68,15 +68,6 @@ export function createAuthTestLifecycle(envKeys: string[]): AuthTestLifecycle { }; } -/** Return OPENCLAW_AGENT_DIR or fail the test clearly. */ -export function requireOpenClawAgentDir(): string { - const agentDir = process.env.OPENCLAW_AGENT_DIR; - if (!agentDir) { - throw new Error("OPENCLAW_AGENT_DIR not set"); - } - return agentDir; -} - /** Resolve the auth profile JSON path for an agent directory. */ function authProfilePathForAgent(agentDir: string): string { return path.join(agentDir, "auth-profiles.json"); diff --git a/test/helpers/config/config-honor-audit.ts b/test/helpers/config/config-honor-audit.ts index a101c019900c..c874e997e806 100644 --- a/test/helpers/config/config-honor-audit.ts +++ b/test/helpers/config/config-honor-audit.ts @@ -27,7 +27,7 @@ type ConfigHonorProofKey = | "testPaths"; /** Result of auditing one config honor inventory. */ -export type ConfigHonorAuditResult = { +type ConfigHonorAuditResult = { schemaKeys: string[]; missingKeys: string[]; extraKeys: string[]; diff --git a/test/helpers/cron/service-regression-fixtures.ts b/test/helpers/cron/service-regression-fixtures.ts index d301c98fa85e..e99d7bcfca94 100644 --- a/test/helpers/cron/service-regression-fixtures.ts +++ b/test/helpers/cron/service-regression-fixtures.ts @@ -149,7 +149,3 @@ export function createIsolatedRegressionJob(params: { state: params.state ?? {}, }; } - -export async function writeCronStoreSnapshot(storePath: string, jobs: unknown[]) { - await fs.writeFile(storePath, JSON.stringify({ version: 1, jobs }), "utf-8"); -} diff --git a/test/helpers/gateway-e2e-harness.ts b/test/helpers/gateway-e2e-harness.ts index 0a2a4f56b8eb..6fa3850ee032 100644 --- a/test/helpers/gateway-e2e-harness.ts +++ b/test/helpers/gateway-e2e-harness.ts @@ -4,13 +4,10 @@ import path from "node:path"; import { GatewayClient } from "../../src/gateway/client.js"; import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js"; import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.js"; -import { extractFirstTextBlock } from "../../src/shared/chat-message-content.js"; import { sleep } from "../../src/utils.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js"; import { createOpenClawTestInstance, type OpenClawTestInstance } from "./openclaw-test-instance.js"; -export { extractFirstTextBlock }; - export type GatewayInstance = OpenClawTestInstance; const GATEWAY_CONNECT_STATUS_TIMEOUT_MS = 10_000; @@ -19,7 +16,7 @@ const GATEWAY_NODE_STATUS_POLL_MS = 20; const POST_JSON_TIMEOUT_MS = 15_000; const POST_JSON_MAX_RESPONSE_BYTES = 1024 * 1024; -export type PostJsonOptions = { +type PostJsonOptions = { maxResponseBytes?: number; timeoutMs?: number; }; diff --git a/test/helpers/image-fixtures.ts b/test/helpers/image-fixtures.ts index 716badbd4016..604850c72aca 100644 --- a/test/helpers/image-fixtures.ts +++ b/test/helpers/image-fixtures.ts @@ -1,8 +1,6 @@ // Image fixture helper re-exports shared media fixture paths. export { - createGrayscaleAlphaPngBuffer, createNoisyPngBuffer, - createNoisyRgbaBuffer, createSolidPngBuffer, createTinyJpegBuffer, } from "../../src/plugin-sdk/test-helpers/image-fixtures.js"; diff --git a/test/helpers/media-generation/bundled-provider-builders.ts b/test/helpers/media-generation/bundled-provider-builders.ts index 478be13cebe7..cead125af2d1 100644 --- a/test/helpers/media-generation/bundled-provider-builders.ts +++ b/test/helpers/media-generation/bundled-provider-builders.ts @@ -1,6 +1,6 @@ // Media generation provider builders create bundled provider fixtures for tests. import type { OpenClawPluginApi } from "../../../src/plugins/types.js"; -import { loadBundledPluginPublicSurfaceSourceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; +import { loadBundledPluginPublicSurface } from "../../../src/test-utils/bundled-plugin-public-surface.js"; // Public-surface loader for bundled media provider plugin tests. @@ -11,11 +11,12 @@ type BundledPluginEntryModule = { }; /** Load a bundled provider plugin entrypoint through the public surface helper. */ -export function loadBundledProviderPlugin(pluginId: string): BundledPluginEntryModule["default"] { - return ( - loadBundledPluginPublicSurfaceSourceSync({ - pluginId, - artifactBasename: "index.js", - }) as BundledPluginEntryModule - ).default; +export async function loadBundledProviderPlugin( + pluginId: string, +): Promise { + const module = await loadBundledPluginPublicSurface({ + pluginId, + artifactBasename: "index.js", + }); + return module.default; } diff --git a/test/helpers/openclaw-test-instance.ts b/test/helpers/openclaw-test-instance.ts index 79749d35f45c..17152734505b 100644 --- a/test/helpers/openclaw-test-instance.ts +++ b/test/helpers/openclaw-test-instance.ts @@ -12,11 +12,12 @@ import { import { createOpenClawTestState, type OpenClawTestState, - type OpenClawTestStateOptions, } from "../../src/test-utils/openclaw-test-state.js"; import { sleep } from "../../src/utils.js"; -export type OpenClawTestInstanceOptions = { +type OpenClawTestStateOptions = NonNullable[0]>; + +type OpenClawTestInstanceOptions = { name: string; cwd?: string; port?: number; @@ -30,7 +31,7 @@ export type OpenClawTestInstanceOptions = { stopTimeoutMs?: number; }; -export type OpenClawTestInstanceCommandResult = { +type OpenClawTestInstanceCommandResult = { code: number | null; signal: NodeJS.Signals | null; stdout: string; diff --git a/test/helpers/plugins/plugin-sdk-stub.cjs b/test/helpers/plugins/plugin-sdk-stub.cjs deleted file mode 100644 index 4cc4a609f921..000000000000 --- a/test/helpers/plugins/plugin-sdk-stub.cjs +++ /dev/null @@ -1,64 +0,0 @@ -// Universal CommonJS plugin-sdk stub for tests that only need import shape compatibility. -"use strict"; - -const stub = new Proxy( - function pluginSdkStub() { - return stub; - }, - { - apply() { - return stub; - }, - construct() { - return stub; - }, - get(_target, prop) { - if (prop === "__esModule") { - return true; - } - if (prop === "default") { - return stub; - } - if (prop === "then") { - return undefined; - } - if (prop === Symbol.toPrimitive) { - return () => ""; - } - if (prop === "toJSON") { - return () => undefined; - } - if (prop === "toString") { - return () => ""; - } - if (prop === "valueOf") { - return () => 0; - } - return stub; - }, - ownKeys(target) { - return [...new Set([...Reflect.ownKeys(target), "__esModule", "default"])]; - }, - getOwnPropertyDescriptor(target, prop) { - if (prop === "__esModule") { - return { - configurable: true, - enumerable: false, - value: true, - writable: false, - }; - } - if (prop === "default") { - return { - configurable: true, - enumerable: false, - value: stub, - writable: false, - }; - } - return Reflect.getOwnPropertyDescriptor(target, prop); - }, - }, -); - -module.exports = stub; diff --git a/test/helpers/sqlite-sessions-transcripts-flip-proof.ts b/test/helpers/sqlite-sessions-transcripts-flip-proof.ts index 2c0efcb3fdd4..bda3dfd46f11 100644 --- a/test/helpers/sqlite-sessions-transcripts-flip-proof.ts +++ b/test/helpers/sqlite-sessions-transcripts-flip-proof.ts @@ -184,7 +184,7 @@ type RollbackRestoreEvidence = { sqliteStillExists: boolean; }; -export type SqliteSessionsTranscriptsFlipProofReport = { +type SqliteSessionsTranscriptsFlipProofReport = { ok: boolean; agentId: string; checkpoints: ProofCheckpoint[]; diff --git a/test/helpers/temp-dir.ts b/test/helpers/temp-dir.ts index 774c9cab3fbb..a2bc1c6fd372 100644 --- a/test/helpers/temp-dir.ts +++ b/test/helpers/temp-dir.ts @@ -5,16 +5,16 @@ import path from "node:path"; // Synchronous temporary directory helpers for tests. -export type TempDirCollection = string[] | Set; -export type RegisterTempDirCleanup = (cleanup: () => void) => unknown; +type TempDirCollection = string[] | Set; +type RegisterTempDirCleanup = (cleanup: () => void) => unknown; -export interface TestTempDirTracker { +interface TestTempDirTracker { readonly dirs: ReadonlySet; make(prefix: string, root?: string): string; cleanup(): void; } -export interface AutoCleanupTempDirTracker { +interface AutoCleanupTempDirTracker { readonly dirs: ReadonlySet; make(prefix: string, root?: string): string; } diff --git a/test/helpers/ui-style-fixtures.ts b/test/helpers/ui-style-fixtures.ts index 0be272970512..eec410149976 100644 --- a/test/helpers/ui-style-fixtures.ts +++ b/test/helpers/ui-style-fixtures.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; -export function resolveStylePath(path: string): string { +function resolveStylePath(path: string): string { const candidates = [resolve(process.cwd(), path), resolve(process.cwd(), "..", path)]; const cssPath = candidates.find((candidate) => existsSync(candidate)); if (!cssPath) { diff --git a/test/image-generation.runtime.live.test.ts b/test/image-generation.runtime.live.test.ts index 26e280911ab1..83cc249208f0 100644 --- a/test/image-generation.runtime.live.test.ts +++ b/test/image-generation.runtime.live.test.ts @@ -271,7 +271,7 @@ describeLive("image generation live (provider sweep)", () => { } const { imageProviders } = await registerProviderPlugin({ - plugin: loadBundledProviderPlugin(providerCase.pluginId), + plugin: await loadBundledProviderPlugin(providerCase.pluginId), id: providerCase.pluginId, name: providerCase.pluginName, }); diff --git a/test/scripts/check-deadcode-exports.test.ts b/test/scripts/check-deadcode-exports.test.ts index 4250807bc91b..64019ba73ef2 100644 --- a/test/scripts/check-deadcode-exports.test.ts +++ b/test/scripts/check-deadcode-exports.test.ts @@ -1,7 +1,9 @@ // Check Deadcode Exports tests cover parsing and hard-zero enforcement. import fs from "node:fs"; import { describe, expect, it } from "vitest"; +import allExportsKnipConfig from "../../config/knip.all-exports.config.ts"; import knipConfig from "../../config/knip.config.ts"; +import scriptExportsKnipConfig from "../../config/knip.scripts-exports.config.ts"; import { checkUnusedExports, parseKnipCompactUnusedExports, @@ -15,23 +17,95 @@ describe("check-deadcode-exports", () => { "utf8", ); expect(script).toContain('"exports,nsExports,types,nsTypes,enumMembers,namespaceMembers"'); + expect(script).toContain('"config/knip.config.ts", "--production"'); + expect(script).toContain('"config/knip.all-exports.config.ts"'); + expect(script).toContain('"config/knip.scripts-exports.config.ts"'); + expect(script).toContain( + 'args: ["--config", "config/knip.scripts-exports.config.ts", "--include-entry-exports"]', + ); }); - it("excludes test support from every Knip issue type", () => { + it("excludes test support only from the production scan", () => { expect(knipConfig.ignore).toContain("dist/**"); expect(knipConfig.ignore).toContain("**/test-helpers/**"); expect(knipConfig.ignore).toContain("**/*.test-utils.ts"); expect(knipConfig.ignoreFiles).not.toContain("**/test-helpers/**"); expect(knipConfig.ignoreFiles).toContain("scripts/**"); + expect(allExportsKnipConfig.ignoreFiles).not.toContain("scripts/**"); expect(knipConfig.ignoreFiles).toContain("dist/**"); expect(knipConfig.ignore).not.toContain("**/live-*.ts"); expect(knipConfig.ignoreFiles).toContain("**/live-*.ts"); + expect(allExportsKnipConfig.ignore).toEqual([ + "dist/**", + "packages/*/dist/**", + "**/.boundary-stubs/**", + ]); + expect(allExportsKnipConfig.ignoreIssues).toHaveProperty("test/fixtures/ts-topology/basic/**"); + expect(knipConfig.workspaces["."].project).toContain("scripts/**/*.{js,mjs,cjs,ts,mts,cts}!"); + }); + + it("makes tests in every workspace roots of the full-tree export audit", () => { + const rootWorkspace = allExportsKnipConfig.workspaces["."]; + const extensionWorkspace = allExportsKnipConfig.workspaces["extensions/*"]; + const uiWorkspace = allExportsKnipConfig.workspaces.ui; + expect(rootWorkspace).toBeDefined(); + expect(extensionWorkspace).toBeDefined(); + expect(uiWorkspace).toBeDefined(); + expect(rootWorkspace!.entry).toEqual( + expect.arrayContaining([ + ".agents/skills/**/scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + "src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "test/vitest/vitest*.config.ts!", + "test/e2e/qa-lab/runtime/media-talk-gateway.ts!", + "test/e2e/qa-lab/runtime/voice-call-gateway.ts!", + ]), + ); + expect(extensionWorkspace!.entry).toContain("**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!"); + expect(uiWorkspace!.entry).toContain("**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!"); + }); + + it("keeps the script unused-export scan scoped to real executable roots", () => { + expect(scriptExportsKnipConfig.workspaces["."].entry).toEqual( + expect.arrayContaining([ + ".agents/skills/**/scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + "scripts/check-live-cache.ts!", + "scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}!", + "src/plugin-sdk/api-baseline.ts!", + ]), + ); + expect(scriptExportsKnipConfig.workspaces["."].entry).not.toContain( + "scripts/**/*.{js,mjs,cjs,ts,mts,cts}!", + ); + expect(scriptExportsKnipConfig.ignoreIssues).toHaveProperty("src/**"); + expect(scriptExportsKnipConfig.ignoreIssues).toHaveProperty( + "scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs", + ); + expect(scriptExportsKnipConfig.ignoreIssues).toHaveProperty( + "scripts/e2e/secret-provider-integrations.mjs", + ); }); it("tracks production script consumers of plugin exports", () => { expect(knipConfig.workspaces["."].entry).toContain("scripts/qa/render-maturity-docs.ts!"); }); + it("runs exhaustive dead-code hygiene against production and full-tree configs", () => { + const packageJson = JSON.parse( + fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"), + ) as { scripts: Record }; + expect(packageJson.scripts["deadcode:dependencies"]).toBe("pnpm deadcode:full"); + expect(packageJson.scripts["deadcode:full"]).toContain( + "--config config/knip.config.ts --production", + ); + expect(packageJson.scripts["deadcode:full"]).toContain( + "--config config/knip.all-exports.config.ts", + ); + expect(packageJson.scripts["deadcode:full"]).toContain("--exclude duplicates"); + }); + it("models the jiti virtual agent-sessions SDK entry", () => { expect(knipConfig.workspaces["."].entry).toContain("src/agents/sessions/extension-sdk.ts!"); }); @@ -40,16 +114,14 @@ describe("check-deadcode-exports", () => { expect(knipConfig.workspaces["."].entry).toContain("src/mcp/openclaw-tools-serve.ts!"); }); - it("keeps every migrated bundled-plugin workspace strict", () => { - const extensionWorkspaces = Object.entries(knipConfig.workspaces).filter( - ([workspace]) => - workspace.startsWith("extensions/") && - !["extensions/*", "extensions/llama-cpp", "extensions/reef"].includes(workspace), + it("scans every nested bundled-plugin source file without broad entry masking", () => { + const extensionWorkspaces = Object.entries(knipConfig.workspaces).filter(([workspace]) => + workspace.startsWith("extensions/"), ); expect(extensionWorkspaces.length).toBeGreaterThan(1); for (const [workspace, settings] of extensionWorkspaces) { expect(settings.entry, workspace).not.toContain("*.ts!"); - expect(settings.project, workspace).toContain("*.ts!"); + expect(settings.project, workspace).toContain("**/*.{js,mjs,ts}!"); expect(settings.entry, workspace).toEqual( expect.arrayContaining([ "index.ts!", @@ -62,10 +134,40 @@ describe("check-deadcode-exports", () => { ]), ); } - expect(knipConfig.workspaces["extensions/*"].entry).toContain("*.ts!"); - expect(knipConfig.workspaces["extensions/*"].project).toContain("*.ts!"); - expect(knipConfig.workspaces["extensions/llama-cpp"].entry).toContain("*.ts!"); - expect(knipConfig.workspaces["extensions/reef"].entry).toContain("*.ts!"); + expect(knipConfig.workspaces["extensions/*"].project).toContain("**/*.{js,mjs,ts}!"); + expect(knipConfig.workspaces["extensions/llama-cpp"].project).toContain("**/*.{js,mjs,ts}!"); + expect(knipConfig.workspaces["extensions/reef"].project).toContain("**/*.{js,mjs,ts}!"); + }); + + it("models non-imported runtime and build entrypoints explicitly", () => { + expect(knipConfig.workspaces["."].entry).toEqual( + expect.arrayContaining([ + "src/agents/subagent-registry.runtime.ts!", + "src/mcp/plugin-tools-serve.ts!", + "src/plugins/build-smoke-entry.ts!", + "src/config/doc-baseline.ts!", + "src/plugins/runtime-sidecar-paths-baseline.ts!", + "tsdown.ai.config.ts!", + ]), + ); + expect(knipConfig.workspaces["extensions/acpx"].entry).toEqual( + expect.arrayContaining([ + "src/runtime-internals/mcp-command-line.mjs!", + "src/runtime-internals/mcp-proxy.mjs!", + ]), + ); + expect(knipConfig.workspaces["extensions/canvas"].entry).toEqual( + expect.arrayContaining([ + "src/host/a2ui-app/bootstrap.js!", + "src/host/a2ui-app/rolldown.config.mjs!", + ]), + ); + expect(knipConfig.workspaces["extensions/diffs"].entry).toContain("src/viewer-client.ts!"); + expect(knipConfig.workspaces["extensions/matrix"].entry).toContain( + "src/plugin-entry.runtime.js!", + ); + expect(knipConfig.workspaces["extensions/mxc"].entry).toContain("src/mxc-spawn-launcher.mjs!"); + expect(knipConfig.workspaces["extensions/qa-lab"].entry).toContain("src/ci-smoke-plan.ts!"); }); it("models the Browser facades loaded by basename", () => { @@ -103,8 +205,10 @@ describe("check-deadcode-exports", () => { it("parses all compact export sections and expands symbol lists", () => { expect( parseKnipCompactUnusedExports(` -Unused exports (2) +Unused exports (4) src/b.ts: beta, alpha +.agents/skills/example/scripts/main.mjs: run +config/knip.extra.config.ts: configEntry /tmp/outside.ts: noise Unused exported types (1) @@ -126,6 +230,8 @@ Unused files (1) src/noise.ts: src/noise.ts `), ).toEqual([ + ".agents/skills/example/scripts/main.mjs: run", + "config/knip.extra.config.ts: configEntry", "extensions/example/src/types.ts: ExampleType", "packages/example/src/state.ts: Ready", "src/b.ts: alpha", diff --git a/test/scripts/check-deadcode-unused-files.test.ts b/test/scripts/check-deadcode-unused-files.test.ts index 92b06e812e64..2a94074a7a14 100644 --- a/test/scripts/check-deadcode-unused-files.test.ts +++ b/test/scripts/check-deadcode-unused-files.test.ts @@ -7,8 +7,8 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { + checkKnipUnusedFileScanResult, checkUnusedFiles, - compareUnusedFilesToAllowlist, KNIP_MAX_BUFFER_BYTES, parseKnipCompactUnusedFiles, runKnipUnusedFiles, @@ -82,6 +82,15 @@ function waitForChildClose( } describe("check-deadcode-unused-files", () => { + it("has no checked-in unused-file allowlist", () => { + expect(existsSync(path.resolve("scripts/deadcode-unused-files.allowlist.mjs"))).toBe(false); + const script = readFileSync(path.resolve("scripts/check-deadcode-unused-files.mjs"), "utf8"); + expect(script).not.toContain("allowlist"); + expect(script).toContain("production and full-tree unused-file checks passed with 0 entries"); + expect(script).toContain('"config/knip.all-exports.config.ts"'); + expect(script).toContain("result.status !== 0"); + }); + it("parses the compact Knip unused-file section", () => { expect( parseKnipCompactUnusedFiles(` @@ -116,71 +125,39 @@ src/a.ts: src/a.ts ).toEqual(["src/a.ts", "src/b.ts"]); }); - it("reports unexpected and stale allowlist entries", () => { - expect( - compareUnusedFilesToAllowlist(["src/a.ts", "src/new.ts"], ["src/a.ts", "src/old.ts"]), - ).toStrictEqual({ - actual: ["src/a.ts", "src/new.ts"], - allowed: ["src/a.ts", "src/old.ts"], - unexpected: ["src/new.ts"], - stale: ["src/old.ts"], - duplicateAllowedCount: 0, - allowlistIsSorted: true, - }); - }); - - it("accepts optional allowlist entries whether Knip reports them or not", () => { - expect( - compareUnusedFilesToAllowlist( - ["src/a.ts", "src/platform.ts"], - ["src/a.ts"], - ["src/platform.ts"], - ), - ).toStrictEqual({ - actual: ["src/a.ts", "src/platform.ts"], - allowed: ["src/a.ts"], - allowlistIsSorted: true, - duplicateAllowedCount: 0, - unexpected: [], - stale: [], - }); - expect( - compareUnusedFilesToAllowlist(["src/a.ts"], ["src/a.ts"], ["src/platform.ts"]), - ).toStrictEqual({ - actual: ["src/a.ts"], - allowed: ["src/a.ts"], - allowlistIsSorted: true, - duplicateAllowedCount: 0, - unexpected: [], - stale: [], - }); - }); - - it("accepts exactly allowlisted unused files", () => { - expect(checkUnusedFiles("Unused files (1)\nsrc/a.ts: src/a.ts\n", ["src/a.ts"])).toStrictEqual({ - comparison: { - actual: ["src/a.ts"], - allowed: ["src/a.ts"], - allowlistIsSorted: true, - duplicateAllowedCount: 0, - stale: [], - unexpected: [], - }, + it("accepts an empty compact report with zero unused files", () => { + expect(checkUnusedFiles("")).toStrictEqual({ + files: [], ok: true, message: "", }); }); - it("rejects unsorted allowlists", () => { + it("rejects a nonzero Knip exit even when no unused files were printed", () => { expect( - compareUnusedFilesToAllowlist(["src/a.ts", "src/b.ts"], ["src/b.ts", "src/a.ts"]), + checkKnipUnusedFileScanResult({ + errorCode: undefined, + output: "", + signal: null, + status: 2, + }), ).toStrictEqual({ - actual: ["src/a.ts", "src/b.ts"], - allowed: ["src/a.ts", "src/b.ts"], - allowlistIsSorted: false, - duplicateAllowedCount: 0, - stale: [], - unexpected: [], + failureReason: "exit status 2", + message: "", + ok: false, + }); + }); + + it("rejects every unused file without an allowlist", () => { + expect( + checkUnusedFiles("Unused files (2)\nsrc/z.ts: src/z.ts\nsrc/a.ts: src/a.ts\n"), + ).toStrictEqual({ + files: ["src/a.ts", "src/z.ts"], + ok: false, + message: `Unused files are not allowed: + src/a.ts + src/z.ts +Delete the files or model their real entrypoints in Knip.`, }); }); @@ -321,7 +298,7 @@ src/a.ts: src/a.ts expect(kills).toContain("SIGTERM"); expect(result).toStrictEqual({ errorCode: "ETIMEDOUT", - errorMessage: expect.stringContaining("Knip unused-file scan timed out"), + errorMessage: expect.stringContaining("Knip production unused-file scan timed out"), output: "", signal: "SIGTERM", status: null, @@ -493,7 +470,7 @@ src/a.ts: src/a.ts await expect(resultPromise).resolves.toStrictEqual({ errorCode: "ENOBUFS", - errorMessage: "Knip unused-file scan exceeded 4 output bytes", + errorMessage: "Knip production unused-file scan exceeded 4 output bytes", output: "too ", signal: "SIGTERM", status: null, diff --git a/test/scripts/plugin-gateway-gauntlet.test.ts b/test/scripts/plugin-gateway-gauntlet.test.ts index 2e969c6c3568..29a4d39cc463 100644 --- a/test/scripts/plugin-gateway-gauntlet.test.ts +++ b/test/scripts/plugin-gateway-gauntlet.test.ts @@ -25,7 +25,6 @@ import { collectQaBaselineRegressionObservations, detectCommandDiagnosticFailure, discoverBundledPluginManifests, - schemaHasRequiredFields, selectPluginEntries, } from "../../scripts/lib/plugin-gateway-gauntlet.mjs"; @@ -340,22 +339,6 @@ describe("plugin gateway gauntlet helpers", () => { ).toThrow("Bundled plugin dependency cycle detected: alpha -> beta -> alpha"); }); - it("detects required schema fields recursively", () => { - expect( - schemaHasRequiredFields({ - type: "object", - properties: { - auth: { - oneOf: [{ type: "object" }, { type: "object", required: ["token"] }], - }, - }, - }), - ).toBe(true); - expect( - schemaHasRequiredFields({ type: "object", properties: { enabled: { type: "boolean" } } }), - ).toBe(false); - }); - it("flags gateway startup CPU observations using bench summary keys", () => { expect( collectGatewayCpuObservations({ diff --git a/test/vitest/vitest.bundled-plugin-paths.ts b/test/vitest/vitest.bundled-plugin-paths.ts index db1682a5dfbf..49f8e7ed238c 100644 --- a/test/vitest/vitest.bundled-plugin-paths.ts +++ b/test/vitest/vitest.bundled-plugin-paths.ts @@ -1,6 +1,5 @@ // Vitest bundled plugin paths helper lists test file patterns for the bundled plugin paths shard. export const BUNDLED_PLUGIN_ROOT_DIR = "extensions"; -export const BUNDLED_PLUGIN_PATH_PREFIX = `${BUNDLED_PLUGIN_ROOT_DIR}/`; export const BUNDLED_PLUGIN_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.test.ts`; export const BUNDLED_PLUGIN_E2E_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.e2e.test.ts`; export const BUNDLED_PLUGIN_LIVE_TEST_GLOB = `${BUNDLED_PLUGIN_ROOT_DIR}/**/*.live.test.ts`; diff --git a/test/vitest/vitest.channel-paths.mjs b/test/vitest/vitest.channel-paths.mjs index d12750e39144..ded0a10ff028 100644 --- a/test/vitest/vitest.channel-paths.mjs +++ b/test/vitest/vitest.channel-paths.mjs @@ -5,18 +5,18 @@ import { splitChannelExtensionTestRoots } from "./vitest.extension-channel-split const normalizeRepoPath = (value) => value.split(path.sep).join("/"); -export const channelTestRoots = ["src/channels", ...splitChannelExtensionTestRoots]; +const channelTestRoots = ["src/channels", ...splitChannelExtensionTestRoots]; const splitChannelExtensionTestRootSet = new Set(splitChannelExtensionTestRoots); -export const extensionChannelTestRoots = channelTestRoots.filter( +const extensionChannelTestRoots = channelTestRoots.filter( (root) => root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX) && !splitChannelExtensionTestRootSet.has(root), ); -export const coreChannelTestRoots = channelTestRoots.filter( +const coreChannelTestRoots = channelTestRoots.filter( (root) => !root.startsWith(BUNDLED_PLUGIN_PATH_PREFIX), ); -export const channelTestPrefixes = channelTestRoots.map((root) => `${root}/`); +const channelTestPrefixes = channelTestRoots.map((root) => `${root}/`); export const extensionChannelTestInclude = extensionChannelTestRoots.map( (root) => `${root}/**/*.test.ts`, ); diff --git a/test/vitest/vitest.extension-channel-split-paths.mjs b/test/vitest/vitest.extension-channel-split-paths.mjs index df476e95b566..8c6f7bff208f 100644 --- a/test/vitest/vitest.extension-channel-split-paths.mjs +++ b/test/vitest/vitest.extension-channel-split-paths.mjs @@ -1,7 +1,7 @@ // Test routing specs for channel plugins split into dedicated Vitest configs. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const splitChannelExtensionShardSpecs = [ +const splitChannelExtensionShardSpecs = [ { id: "discord", kind: "extensionDiscord", diff --git a/test/vitest/vitest.extension-feishu-paths.mjs b/test/vitest/vitest.extension-feishu-paths.mjs index 36af38b7a5a9..52d681417060 100644 --- a/test/vitest/vitest.extension-feishu-paths.mjs +++ b/test/vitest/vitest.extension-feishu-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for Feishu extension tests. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const feishuExtensionIds = ["feishu"]; +const feishuExtensionIds = ["feishu"]; export const feishuExtensionTestRoots = feishuExtensionIds.map((id) => bundledPluginRoot(id)); diff --git a/test/vitest/vitest.extension-matrix-paths.mjs b/test/vitest/vitest.extension-matrix-paths.mjs index efb9bd50dca1..a8caabe206ab 100644 --- a/test/vitest/vitest.extension-matrix-paths.mjs +++ b/test/vitest/vitest.extension-matrix-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for Matrix extension tests. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const matrixExtensionIds = ["matrix"]; +const matrixExtensionIds = ["matrix"]; export const matrixExtensionTestRoots = matrixExtensionIds.map((id) => bundledPluginRoot(id)); diff --git a/test/vitest/vitest.extension-mattermost-paths.mjs b/test/vitest/vitest.extension-mattermost-paths.mjs index 8e1cd50d4e19..7806e4302c3c 100644 --- a/test/vitest/vitest.extension-mattermost-paths.mjs +++ b/test/vitest/vitest.extension-mattermost-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for Mattermost extension tests. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const mattermostExtensionIds = ["mattermost"]; +const mattermostExtensionIds = ["mattermost"]; export const mattermostExtensionTestRoots = mattermostExtensionIds.map((id) => bundledPluginRoot(id), diff --git a/test/vitest/vitest.extension-messaging-paths.mjs b/test/vitest/vitest.extension-messaging-paths.mjs index 27c2fd0271d7..eb61dccc474c 100644 --- a/test/vitest/vitest.extension-messaging-paths.mjs +++ b/test/vitest/vitest.extension-messaging-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for miscellaneous messaging extension suites. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const messagingExtensionIds = [ +const messagingExtensionIds = [ "googlechat", "nextcloud-talk", "nostr", diff --git a/test/vitest/vitest.extension-msteams-paths.mjs b/test/vitest/vitest.extension-msteams-paths.mjs index 873855963d64..f0ec02b357e7 100644 --- a/test/vitest/vitest.extension-msteams-paths.mjs +++ b/test/vitest/vitest.extension-msteams-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for Microsoft Teams extension tests. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const msTeamsExtensionIds = ["msteams"]; +const msTeamsExtensionIds = ["msteams"]; export const msTeamsExtensionTestRoots = msTeamsExtensionIds.map((id) => bundledPluginRoot(id)); diff --git a/test/vitest/vitest.extension-provider-paths.mjs b/test/vitest/vitest.extension-provider-paths.mjs index d2f25f7274ad..ac0b098195ca 100644 --- a/test/vitest/vitest.extension-provider-paths.mjs +++ b/test/vitest/vitest.extension-provider-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for model provider extension suites. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const providerExtensionIds = [ +const providerExtensionIds = [ "amazon-bedrock", "amazon-bedrock-mantle", "anthropic", @@ -34,7 +34,7 @@ export const providerExtensionIds = [ "zai", ]; -export const providerOpenAiExtensionIds = ["openai"]; +const providerOpenAiExtensionIds = ["openai"]; export const providerExtensionTestRoots = providerExtensionIds.map((id) => bundledPluginRoot(id)); export const providerOpenAiExtensionTestRoots = providerOpenAiExtensionIds.map((id) => diff --git a/test/vitest/vitest.extension-voice-call-paths.mjs b/test/vitest/vitest.extension-voice-call-paths.mjs index 28f70a26da8a..7fb2f0675159 100644 --- a/test/vitest/vitest.extension-voice-call-paths.mjs +++ b/test/vitest/vitest.extension-voice-call-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for voice-call extension tests. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const voiceCallExtensionIds = ["voice-call"]; +const voiceCallExtensionIds = ["voice-call"]; export const voiceCallExtensionTestRoots = voiceCallExtensionIds.map((id) => bundledPluginRoot(id)); diff --git a/test/vitest/vitest.extension-whatsapp-paths.mjs b/test/vitest/vitest.extension-whatsapp-paths.mjs index abfded413a52..721c7264c10d 100644 --- a/test/vitest/vitest.extension-whatsapp-paths.mjs +++ b/test/vitest/vitest.extension-whatsapp-paths.mjs @@ -1,7 +1,7 @@ // Test routing roots for WhatsApp extension tests. import { bundledPluginRoot } from "../../scripts/lib/bundled-plugin-paths.mjs"; -export const whatsAppExtensionIds = ["whatsapp"]; +const whatsAppExtensionIds = ["whatsapp"]; export const whatsAppExtensionTestRoots = whatsAppExtensionIds.map((id) => bundledPluginRoot(id)); diff --git a/test/vitest/vitest.system-load.ts b/test/vitest/vitest.system-load.ts index 7a8466fabb23..87c28dabcf36 100644 --- a/test/vitest/vitest.system-load.ts +++ b/test/vitest/vitest.system-load.ts @@ -2,7 +2,7 @@ type EnvMap = Record; -export type VitestProcessStats = { +type VitestProcessStats = { otherVitestRootCount: number; otherVitestWorkerCount: number; otherVitestCpuPercent: number; diff --git a/test/vitest/vitest.unit-fast-paths.mjs b/test/vitest/vitest.unit-fast-paths.mjs index 3ad98109b0e3..59a93c0c4d1b 100644 --- a/test/vitest/vitest.unit-fast-paths.mjs +++ b/test/vitest/vitest.unit-fast-paths.mjs @@ -221,7 +221,7 @@ const broadUnitFastCandidateGlobs = [ "packages/**/*.test.ts", "test/**/*.test.ts", ]; -export const ownerRoutedUnitTestFiles = [ +const ownerRoutedUnitTestFiles = [ "src/agents/openai-transport-stream.test.ts", "src/auto-reply/reply/dispatch-from-config.test.ts", ]; @@ -548,10 +548,7 @@ function analyzeUnitFastTestFile(cwd, file) { const forced = forcedUnitFastTestFileSet.has(file); analysis = { file, - unitFast: - forced || - reasons.length === 0 || - reasons.every((reason) => reason === "stateful-test-helper"), + unitFast: forced || reasons.every((reason) => reason === "stateful-test-helper"), forced, reasons, }; @@ -602,7 +599,7 @@ export function getUnitFastTestFilesForIncludePatterns(includePatterns, options } const patterns = [ ...new Set(normalizedPatterns.map((pattern) => anchorScopedIncludePattern(pattern, dir))), - ].toSorted(); + ].toSorted((left, right) => left.localeCompare(right)); const cacheKey = JSON.stringify([normalizedCwd, dir, patterns]); const cached = scopedUnitFastTestFilesByKey.get(cacheKey); if (cached) { diff --git a/ui/package.json b/ui/package.json index 3767e7386393..297cb9a57a16 100644 --- a/ui/package.json +++ b/ui/package.json @@ -41,8 +41,10 @@ }, "devDependencies": { "@types/markdown-it": "14.1.2", + "@vitest/browser": "4.1.9", "@vitest/browser-playwright": "4.1.9", "jsdom": "29.1.1", + "openclaw": "workspace:*", "playwright": "1.61.1", "vite": "8.1.3", "vitest": "4.1.9"