mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 09:31:10 +00:00
refactor(deadcode): enforce repository hard zero (#108641)
This commit is contained in:
committed by
GitHub
parent
cf8cfa8322
commit
bae9752c5a
@@ -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) {
|
||||
|
||||
@@ -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]),
|
||||
|
||||
132
config/knip.all-exports.config.ts
Normal file
132
config/knip.all-exports.config.ts
Normal file
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
76
config/knip.scripts-exports.config.ts
Normal file
76
config/knip.scripts-exports.config.ts
Normal file
@@ -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;
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -127,10 +127,6 @@ export async function writeCodexAppServerBinding(
|
||||
await testCodexAppServerBindingStore.mutate(testIdentity(sessionId), { kind: "set", binding });
|
||||
}
|
||||
|
||||
export async function clearCodexAppServerBinding(sessionId: string): Promise<void> {
|
||||
await testCodexAppServerBindingStore.mutate(testIdentity(sessionId), { kind: "clear" });
|
||||
}
|
||||
|
||||
export async function clearCodexAppServerBindingForThread(
|
||||
sessionId: string,
|
||||
threadId: string,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"@github/copilot-sdk": "1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@github/copilot": "1.0.68",
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(() => {});
|
||||
@@ -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<string, unknown> = {}): OpenClawConfig {
|
||||
return {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -283,5 +283,3 @@ export function buildCaptureTimelineModel(params: {
|
||||
visibleTimelineLanes,
|
||||
};
|
||||
}
|
||||
|
||||
export type CaptureTimelineModel = ReturnType<typeof buildCaptureTimelineModel>;
|
||||
|
||||
@@ -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<Record<string, string | number | null>>;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -7,7 +7,7 @@ type RuntimeConfigApi = ReturnType<NonNullable<CommandsPort["approveRuntimeGette
|
||||
type ReplaceConfigFile = RuntimeConfigApi["replaceConfigFile"];
|
||||
type ReplaceConfigFileResult = Awaited<ReturnType<ReplaceConfigFile>>;
|
||||
|
||||
export type WrittenQQBotConfig = {
|
||||
type WrittenQQBotConfig = {
|
||||
streaming?: unknown;
|
||||
accounts?: { default?: { streaming?: unknown } };
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -173,7 +173,7 @@ export function resetSlackThreadStarterCacheForTest(): void {
|
||||
THREAD_STARTER_CACHE.clear();
|
||||
}
|
||||
|
||||
export type SlackThreadMessage = {
|
||||
type SlackThreadMessage = {
|
||||
text: string;
|
||||
userId?: string;
|
||||
ts?: string;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -165,7 +165,7 @@ function installTopicNameRuntimeForTest(): void {
|
||||
} as TelegramRuntime);
|
||||
}
|
||||
|
||||
export const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = {
|
||||
const telegramBotRuntimeForTest: TelegramBotRuntimeForTest = {
|
||||
Bot: class {
|
||||
api = apiStub;
|
||||
use = middlewareUseSpy;
|
||||
|
||||
@@ -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";
|
||||
@@ -92,7 +92,7 @@ function createVoiceCallStateRuntimeForTests(): VoiceCallStateRuntime["state"] {
|
||||
};
|
||||
}
|
||||
|
||||
export function installVoiceCallStateRuntimeForTests(): void {
|
||||
function installVoiceCallStateRuntimeForTests(): void {
|
||||
setVoiceCallStateRuntime({ state: createVoiceCallStateRuntimeForTests() });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Whatsapp plugin module implements constants behavior.
|
||||
export { DEFAULT_WEB_MEDIA_BYTES } from "./src/auto-reply/constants.js";
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
resetLoadConfigMock as _resetLoadConfigMock,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
export { createAcceptedWhatsAppSendResult } from "./inbound/send-result.test-helper.js";
|
||||
export {
|
||||
resetLoadConfigMock,
|
||||
setLoadConfigMock,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// Whatsapp plugin module implements targets behavior.
|
||||
export {
|
||||
isWhatsAppGroupJid,
|
||||
isWhatsAppUserTarget,
|
||||
normalizeWhatsAppTarget,
|
||||
} from "./src/normalize-target.js";
|
||||
10
package.json
10
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": {
|
||||
|
||||
@@ -92,7 +92,7 @@ function isThinkingPart(part: Pick<Part, "thought" | "thoughtSignature">): 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<T extends GoogleApiType>(
|
||||
}
|
||||
|
||||
/** @internal Directly tested provider implementation detail. */
|
||||
export function isGemma4Model<T extends GoogleApiType>(model: Model<T>): boolean {
|
||||
function isGemma4Model<T extends GoogleApiType>(model: Model<T>): boolean {
|
||||
return /gemma-?4/.test(model.id.toLowerCase());
|
||||
}
|
||||
|
||||
@@ -701,7 +701,7 @@ function getGoogleBudget<T extends GoogleApiType>(
|
||||
* 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";
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<string, unknown>): Promise<unknown>;
|
||||
/**
|
||||
* 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<string, unknown>,
|
||||
): Promise<KnipUnusedFileScanResult>;
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
];
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -9,8 +9,6 @@ export type PluginInstallRecord = Record<string, unknown> & {
|
||||
spec?: string;
|
||||
};
|
||||
|
||||
export function stateDir(): string;
|
||||
export function configPath(): string;
|
||||
export function readPluginInstallIndex(options?: Record<string, unknown>): unknown;
|
||||
export function readPluginInstallRecords(options?: {
|
||||
configPath?: string;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<T>(filePath: string): Promise<T> {
|
||||
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<string> {
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -2164,13 +2164,10 @@ async function main() {
|
||||
|
||||
export {
|
||||
assertAllowedFailureCommandSucceeded,
|
||||
clampSecretProofTimerTimeoutMs,
|
||||
collectBlockingProofResults,
|
||||
cleanupEnv,
|
||||
expectGatewayStartupFails,
|
||||
gatewayCall,
|
||||
parseJsonOutput,
|
||||
readPositiveTimerMs,
|
||||
runPtySecretsConfigurePreset,
|
||||
runWithProof,
|
||||
runCommand,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}`);
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@ export const selectedCodexAppServerJsonSchemas = [
|
||||
"v2/TurnStartResponse.json",
|
||||
] as const;
|
||||
|
||||
export type GeneratedCodexAppServerProtocolSource = {
|
||||
type GeneratedCodexAppServerProtocolSource = {
|
||||
root: string;
|
||||
codexRepo: string;
|
||||
typescriptRoot: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, number>;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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<ArrayBuffer>;
|
||||
export function spawnPlainGh(
|
||||
args: readonly string[],
|
||||
options: SpawnSyncOptionsWithStringEncoding,
|
||||
): SpawnSyncReturns<string>;
|
||||
export function spawnPlainGh(
|
||||
args: readonly string[],
|
||||
options?: SpawnSyncOptionsWithBufferEncoding,
|
||||
): SpawnSyncReturns<Uint8Array<ArrayBuffer>>;
|
||||
export function spawnPlainGh(
|
||||
args: readonly string[],
|
||||
options?: SpawnSyncOptions,
|
||||
): SpawnSyncReturns<string | Uint8Array<ArrayBuffer>>;
|
||||
export const PLAIN_GH_SYSTEM_CANDIDATES: string[];
|
||||
|
||||
@@ -126,7 +126,6 @@ export function readQaSuiteSummary(summaryPath: unknown):
|
||||
diagnosticDetail: null;
|
||||
summary: unknown;
|
||||
};
|
||||
export function schemaHasRequiredFields(schema: unknown, seen?: Set<unknown>): boolean;
|
||||
export function selectPluginEntries<T extends PluginGatewayEntry>(
|
||||
entries: T[],
|
||||
options?: { ids?: string[]; shardIndex?: number; shardTotal?: number },
|
||||
|
||||
@@ -633,6 +633,5 @@ export {
|
||||
detectCommandDiagnosticFailure,
|
||||
discoverBundledPluginManifests,
|
||||
readQaSuiteSummary,
|
||||
schemaHasRequiredFields,
|
||||
selectPluginEntries,
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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] <profile...>";
|
||||
}
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -40,7 +40,7 @@ type MatrixCell = {
|
||||
type ChromiumLauncher = Awaited<typeof import("playwright")>["chromium"];
|
||||
type ChromiumBrowser = Awaited<ReturnType<ChromiumLauncher["launch"]>>;
|
||||
|
||||
export type ProducerOptions = {
|
||||
type ProducerOptions = {
|
||||
artifactBase: string;
|
||||
repoRoot: string;
|
||||
skipVisualProof: boolean;
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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.`);
|
||||
@@ -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],
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
@@ -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);
|
||||
@@ -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> | AcpRuntime;
|
||||
agentId?: string;
|
||||
successPrompt?: string;
|
||||
errorPrompt?: string;
|
||||
includeControlChecks?: boolean;
|
||||
assertSuccessEvents?: (events: AcpRuntimeEvent[]) => void | Promise<void>;
|
||||
assertErrorOutcome?: (params: {
|
||||
events: AcpRuntimeEvent[];
|
||||
thrown: unknown;
|
||||
}) => void | Promise<void>;
|
||||
};
|
||||
|
||||
/** Runs the shared behavioral contract for ACP runtime adapters. */
|
||||
export async function runAcpRuntimeAdapterContract(
|
||||
params: AcpRuntimeAdapterContractParams,
|
||||
): Promise<void> {
|
||||
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",
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -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<MockMemorySearchManager>
|
||||
> = 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,
|
||||
);
|
||||
|
||||
@@ -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<EmbeddedRunAttemptResult>>();
|
||||
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<MockAssistantErrorProbe>(() => 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<MockAssistantErrorProbe>(() => 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",
|
||||
|
||||
237
src/agents/live-cache-regression-policy.ts
Normal file
237
src/agents/live-cache-regression-policy.ts
Normal file
@@ -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<typeof resolveLiveDirectModelPool>[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<string, Record<string, unknown>>;
|
||||
type LiveCacheProviderResolver = (
|
||||
params: LiveCacheProviderConfig,
|
||||
) => Promise<LiveResolvedModelPool>;
|
||||
|
||||
export async function resolveLiveCacheProviderPool(params: {
|
||||
config: LiveCacheProviderConfig;
|
||||
regressions: string[];
|
||||
resolver?: LiveCacheProviderResolver;
|
||||
summary: LiveCacheRegressionSummary;
|
||||
warnings: string[];
|
||||
}): Promise<LiveResolvedModelPool | undefined> {
|
||||
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 ")
|
||||
);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<typeof resolveLiveDirectModelPool>[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<string, Record<string, unknown>>;
|
||||
warnings: string[];
|
||||
};
|
||||
type LiveCacheRegressionSummary = LiveCacheRegressionResult["summary"];
|
||||
type LiveCacheProviderResolver = (
|
||||
params: LiveCacheProviderConfig,
|
||||
) => Promise<LiveResolvedModelPool>;
|
||||
|
||||
class CacheProbeTextMismatchError extends Error {
|
||||
constructor(
|
||||
readonly suffix: string,
|
||||
@@ -108,34 +83,6 @@ function makeUserTurn(content: Extract<Message, { role: "user" }>["content"]): M
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveLiveCacheProviderPool(params: {
|
||||
config: LiveCacheProviderConfig;
|
||||
regressions: string[];
|
||||
resolver?: LiveCacheProviderResolver;
|
||||
summary: LiveCacheRegressionSummary;
|
||||
warnings: string[];
|
||||
}): Promise<LiveResolvedModelPool | undefined> {
|
||||
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<LiveCacheRegressionResult> {
|
||||
const pngBase64 = (await fs.readFile(LIVE_TEST_PNG_URL)).toString("base64");
|
||||
@@ -881,4 +653,3 @@ export async function runLiveCacheRegression(): Promise<LiveCacheRegressionResul
|
||||
}
|
||||
return { regressions, summary, warnings };
|
||||
}
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
20
src/agents/live-test-provider-drift.test-support.ts
Normal file
20
src/agents/live-test-provider-drift.test-support.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js";
|
||||
|
||||
export function isLiveAuthDrift(error: unknown): boolean {
|
||||
return shouldSkipLiveProviderDrift({ allowAuth: true, error })?.reason === "auth";
|
||||
}
|
||||
|
||||
export function isLiveBillingDrift(error: unknown): boolean {
|
||||
return shouldSkipLiveProviderDrift({ allowBilling: true, error })?.reason === "billing";
|
||||
}
|
||||
|
||||
export function isLiveRateLimitDrift(error: unknown): boolean {
|
||||
return shouldSkipLiveProviderDrift({ allowRateLimit: true, error })?.reason === "rate-limit";
|
||||
}
|
||||
|
||||
export function isLiveProviderUnavailableDrift(error: unknown): boolean {
|
||||
return (
|
||||
shouldSkipLiveProviderDrift({ allowProviderUnavailable: true, error })?.reason ===
|
||||
"provider-unavailable"
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
// Classifies acceptable live-provider drift for optional validation lanes.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldSkipLiveProviderDrift } from "./live-test-provider-drift.js";
|
||||
import {
|
||||
isLiveAuthDrift,
|
||||
isLiveBillingDrift,
|
||||
isLiveProviderUnavailableDrift,
|
||||
isLiveRateLimitDrift,
|
||||
shouldSkipLiveProviderDrift,
|
||||
} from "./live-test-provider-drift.js";
|
||||
} from "./live-test-provider-drift.test-support.js";
|
||||
|
||||
describe("live test provider drift", () => {
|
||||
it("classifies provider account drift", () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user