From 8b399c7798008edbcda977db6d1459cabfbbe325 Mon Sep 17 00:00:00 2001 From: limityan Date: Mon, 3 Aug 2026 21:28:02 +0800 Subject: [PATCH] perf(test): isolate runtime test support and CLI contracts --- scripts/check-core-boundaries.test.mjs | 131 ++++++++++++++++++ .../cargo-dependency-boundaries.mjs | 89 ++++++++++++ scripts/core-boundaries/checker.mjs | 24 +++- .../explicit-test-topology.mjs | 52 ++++++- .../rules/source/required-rules.mjs | 25 +++- scripts/core-boundaries/self-test.mjs | 2 +- src/apps/cli/Cargo.toml | 13 ++ src/apps/cli/tests/cli_command_contracts.rs | 10 ++ .../compat_entrypoint.rs | 0 .../exec_cli_contracts.rs | 1 + .../plugin_source_cli.rs | 0 .../product_assembly_cli.rs | 64 ++++----- src/crates/assembly/core/Cargo.toml | 1 + .../assembly/product-capabilities/Cargo.toml | 1 + src/crates/execution/agent-runtime/Cargo.toml | 1 + .../execution/runtime-services/Cargo.toml | 4 + .../execution/runtime-services/src/lib.rs | 3 + .../runtime_services_contracts.rs | 10 +- 18 files changed, 383 insertions(+), 48 deletions(-) create mode 100644 src/apps/cli/tests/cli_command_contracts.rs rename src/apps/cli/tests/{ => cli_command_contracts}/compat_entrypoint.rs (100%) rename src/apps/cli/tests/{ => cli_command_contracts}/exec_cli_contracts.rs (99%) rename src/apps/cli/tests/{ => cli_command_contracts}/plugin_source_cli.rs (100%) rename src/apps/cli/tests/{ => cli_command_contracts}/product_assembly_cli.rs (86%) rename src/crates/execution/runtime-services/{tests => src}/runtime_services_contracts.rs (98%) diff --git a/scripts/check-core-boundaries.test.mjs b/scripts/check-core-boundaries.test.mjs index 69a95917aa..8d242c6a9d 100644 --- a/scripts/check-core-boundaries.test.mjs +++ b/scripts/check-core-boundaries.test.mjs @@ -12,11 +12,17 @@ import { findFeatureGatedTestTargetViolations, findProductEntrypointCoreFeatureViolations, findReqwestDependencyFeatureViolations, + findRuntimeServicesTestSupportFeatureViolations, findResolvedReqwestNativeTlsViolations, findServicesIntegrationsReqwestFeatureViolations, findServicesIntegrationsTokioFeatureViolations, findTokioDependencyFeatureViolations, } from './core-boundaries/cargo-dependency-boundaries.mjs'; +import { + checkCliIntegrationTestTopology, + cliIntegrationTestTargets, + validateExplicitIntegrationTestTopology, +} from './core-boundaries/explicit-test-topology.mjs'; import { crateLayoutRules } from './core-boundaries/rules/crate-layout.mjs'; const ENTRYPOINT = new URL('./check-core-boundaries.mjs', import.meta.url); @@ -131,6 +137,131 @@ test('matching integration target requirements cover all positive crate features ); }); +test('runtime-services test support stays dev-only across dependency and feature edges', () => { + const runtimeServicesPath = 'src/crates/execution/runtime-services'; + const packages = [ + packageAt('normal-consumer', 'src/apps/normal/Cargo.toml', [ + pathDependency(runtimeServicesPath, { + name: 'bitfun-runtime-services', + features: ['test-support'], + }), + ]), + packageAt('build-consumer', 'src/apps/build/Cargo.toml', [ + pathDependency(runtimeServicesPath, { + name: 'bitfun-runtime-services', + kind: 'build', + features: ['test-support'], + }), + ]), + { + ...packageAt('feature-forwarder', 'src/apps/forwarder/Cargo.toml'), + features: { + preview: ['bitfun-runtime-services/test-support'], + }, + }, + { + ...packageAt('weak-forwarder', 'src/apps/weak/Cargo.toml'), + features: { + preview: ['bitfun-runtime-services?/test-support'], + }, + }, + { + ...packageAt('renamed-forwarder', 'src/apps/renamed/Cargo.toml', [{ + ...pathDependency(runtimeServicesPath, { + name: 'bitfun-runtime-services', + optional: true, + }), + rename: 'runtime_services', + }]), + features: { + preview: ['runtime_services?/test-support'], + }, + }, + { + ...packageAt( + 'bitfun-runtime-services', + 'src/crates/execution/runtime-services/Cargo.toml', + ), + features: { + default: ['test-support'], + 'test-support': [], + }, + }, + packageAt('test-consumer', 'src/apps/test/Cargo.toml', [ + pathDependency(runtimeServicesPath, { + name: 'bitfun-runtime-services', + kind: 'dev', + features: ['test-support'], + }), + ]), + ]; + + const violations = findRuntimeServicesTestSupportFeatureViolations(packages); + + assert.equal(violations.length, 6); + assert.match(violations[0].message, /normal-consumer.*normal dependency/); + assert.match(violations[1].message, /build-consumer.*build dependency/); + assert.match(violations[2].message, /feature-forwarder:preview/); + assert.match(violations[3].message, /weak-forwarder:preview/); + assert.match(violations[4].message, /renamed-forwarder:preview/); + assert.match(violations[5].message, /bitfun-runtime-services:default/); +}); + +test('runtime-services feature aliases cannot hide test support from default builds', () => { + const owner = { + ...packageAt( + 'bitfun-runtime-services', + 'src/crates/execution/runtime-services/Cargo.toml', + ), + features: { + default: ['testing'], + testing: ['test-support'], + 'test-support': [], + }, + }; + + const messages = findRuntimeServicesTestSupportFeatureViolations([owner]) + .map((violation) => violation.message) + .join('\n'); + + assert.match(messages, /bitfun-runtime-services:default/); + assert.match(messages, /default -> testing -> test-support/); + assert.match(messages, /bitfun-runtime-services:testing/); +}); + +test('CLI integration tests keep the reviewed three-target topology', () => { + const repositoryRoot = fileURLToPath(new URL('..', import.meta.url)); + + assert.deepEqual(cliIntegrationTestTargets, [ + { name: 'acp_stdio_cli', path: 'tests/acp_stdio_cli.rs' }, + { name: 'cli_command_contracts', path: 'tests/cli_command_contracts.rs' }, + { name: 'terminal_process_contracts', path: 'tests/terminal_process_contracts.rs' }, + ]); + assert.deepEqual(checkCliIntegrationTestTopology(repositoryRoot), []); +}); + +test('runtime-services test support is absent from ordinary library builds', async () => { + const [manifest, library] = await Promise.all([ + readFile( + new URL('../src/crates/execution/runtime-services/Cargo.toml', import.meta.url), + 'utf8', + ), + readFile( + new URL('../src/crates/execution/runtime-services/src/lib.rs', import.meta.url), + 'utf8', + ), + ]); + + assert.match(manifest, /^test-support\s*=\s*\[\]\s*$/m); + assert.doesNotMatch(manifest, /^required-features\s*=.*test-support.*$/m); + assert.match( + library, + /#\[cfg\(any\(test, feature = "test-support"\)\)\]\s*pub mod test_support;/, + ); + assert.match(library, /#\[cfg\(test\)\]\s*mod runtime_services_contracts;/); + assert.equal((library.match(/^pub mod test_support;\s*$/gm) ?? []).length, 1); +}); + test('feature-gated integration targets reject extra umbrella requirements', () => { const sourcePath = join(TEST_ROOT, 'tests', 'focused.rs'); const pkg = { diff --git a/scripts/core-boundaries/cargo-dependency-boundaries.mjs b/scripts/core-boundaries/cargo-dependency-boundaries.mjs index 66af03cabb..c2c39451ed 100644 --- a/scripts/core-boundaries/cargo-dependency-boundaries.mjs +++ b/scripts/core-boundaries/cargo-dependency-boundaries.mjs @@ -325,6 +325,94 @@ export function findReqwestDependencyFeatureViolations(packages) { }); } +export function findRuntimeServicesTestSupportFeatureViolations(packages) { + const violations = []; + + const pathToFeature = (featureGraph, start, target, visiting = new Set()) => { + if (start === target) { + return [target]; + } + if (visiting.has(start)) { + return null; + } + visiting.add(start); + for (const reference of featureGraph[start] ?? []) { + if (!Object.hasOwn(featureGraph, reference)) { + continue; + } + const suffix = pathToFeature(featureGraph, reference, target, visiting); + if (suffix) { + visiting.delete(start); + return [start, ...suffix]; + } + } + visiting.delete(start); + return null; + }; + + for (const pkg of packages) { + const runtimeServiceAliases = new Set(['bitfun-runtime-services']); + for (const dependency of pkg.dependencies ?? []) { + if (dependency.name !== 'bitfun-runtime-services') { + continue; + } + runtimeServiceAliases.add(dependency.rename ?? dependency.name); + if ( + (dependency.features ?? []).includes('test-support') + && dependency.kind !== 'dev' + ) { + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name} must not enable bitfun-runtime-services/test-support for its ` + + dependencyDescription(dependency), + }); + } + } + + for (const [featureName, references] of Object.entries(pkg.features ?? {})) { + const testSupportReference = references.find((reference) => + [...runtimeServiceAliases].some( + (alias) => + reference === `${alias}/test-support` + || reference === `${alias}?/test-support`, + )); + if (!testSupportReference) { + continue; + } + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `${pkg.name}:${featureName} must not expose bitfun-runtime-services/test-support ` + + 'through a package feature', + }); + } + + if (pkg.name === 'bitfun-runtime-services') { + for (const featureName of Object.keys(pkg.features ?? {})) { + if (featureName === 'test-support') { + continue; + } + const path = pathToFeature(pkg.features, featureName, 'test-support'); + if (!path) { + continue; + } + violations.push({ + path: pkg.manifest_path, + line: 1, + message: + `bitfun-runtime-services:${featureName} must not expose test-support; ` + + `reachable via ${path.join(' -> ')}`, + }); + } + } + } + + return violations; +} + export function findResolvedReqwestNativeTlsViolations(records, { root }) { const reqwestRecords = records.filter((record) => record.name === 'reqwest'); if (reqwestRecords.length === 0) { @@ -1076,6 +1164,7 @@ export function checkCargoDependencyBoundaries({ root, crateLayoutRules }) { { root, crateLayoutRules }, ), ...findFeatureGatedTestTargetViolations(packages), + ...findRuntimeServicesTestSupportFeatureViolations(packages), ...findTokioDependencyFeatureViolations(packages), ...findReqwestDependencyFeatureViolations(packages), ...findResolvedReqwestNativeTlsViolations(resolvedPackageFeatures, { root }), diff --git a/scripts/core-boundaries/checker.mjs b/scripts/core-boundaries/checker.mjs index 66aea673bf..942fedad31 100644 --- a/scripts/core-boundaries/checker.mjs +++ b/scripts/core-boundaries/checker.mjs @@ -38,6 +38,8 @@ import { checkCargoDependencyBoundariesSafely } from './cargo-dependency-boundar import { agentRuntimeIntegrationTestTargets, checkAgentRuntimeIntegrationTestTopology, + checkCliIntegrationTestTopology, + cliIntegrationTestTargets, validateExplicitIntegrationTestTopology, } from './explicit-test-topology.mjs'; @@ -121,6 +123,17 @@ function isDependencyListHeader(trimmedLine, options = {}) { return new RegExp(`^\\[(?:target\\.[^\\]]+\\.)?${workspacePrefix}(?:dependencies|dev-dependencies|build-dependencies)\\]$`).test(trimmedLine); } +function dependencyKindForHeader(trimmedLine, options = {}) { + const workspacePrefix = options.includeWorkspace ? '(?:workspace\\.)?' : ''; + const match = trimmedLine.match( + new RegExp(`^\\[(?:target\\.[^\\]]+\\.)?${workspacePrefix}(dependencies|dev-dependencies|build-dependencies)(?:\\.|\\])`), + ); + if (!match || match[1] === 'dependencies') { + return 'normal'; + } + return match[1] === 'dev-dependencies' ? 'dev' : 'build'; +} + function dependencyTablePattern(options = {}) { const workspacePrefix = options.includeWorkspace ? '(?:workspace\\.)?' : ''; return new RegExp(`^\\[(?:target\\.[^\\]]+\\.)?${workspacePrefix}(?:dependencies|dev-dependencies|build-dependencies)\\.([A-Za-z0-9_-]+|"[A-Za-z0-9_-]+")\\]$`); @@ -129,6 +142,7 @@ function dependencyTablePattern(options = {}) { function parseManifestDependencies(lines, options = {}) { const deps = []; let inDependencyList = false; + let dependencyListKind = 'normal'; let currentTable = null; let currentInline = null; const tablePattern = dependencyTablePattern(options); @@ -153,12 +167,14 @@ function parseManifestDependencies(lines, options = {}) { const headerMatch = trimmed.match(/^\[(.+)]$/); if (headerMatch) { inDependencyList = isDependencyListHeader(trimmed, options); + dependencyListKind = dependencyKindForHeader(trimmed, options); currentTable = null; const dependencyTableMatch = trimmed.match(tablePattern); if (dependencyTableMatch) { currentTable = { name: dependencyTableMatch[1].replace(/^"|"$/g, ''), line: index + 1, + kind: dependencyListKind, optional: false, text: [trimmed], }; @@ -185,6 +201,7 @@ function parseManifestDependencies(lines, options = {}) { deps.push({ name, line: index + 1, + kind: dependencyListKind, optional: /\boptional\s*=\s*true\b/.test(trimmed), text: [trimmed], }); @@ -494,7 +511,8 @@ function checkOptionalDependencyFeatureOwners(crateDir, rule) { const manifestPath = join(crateDir, 'Cargo.toml'); const lines = readText(manifestPath).split(/\r?\n/); const deps = parseManifestDependencies(lines); - const depsByName = new Map(deps.map((dep) => [dep.name, dep])); + const normalDeps = deps.filter((dep) => dep.kind === 'normal'); + const depsByName = new Map(normalDeps.map((dep) => [dep.name, dep])); const features = parseManifestFeatures(lines); const declaredOwnerDeps = new Set(rule.dependencies.map((dependency) => dependency.depName)); @@ -545,7 +563,7 @@ function checkOptionalDependencyFeatureOwners(crateDir, rule) { const profileRule = dependencyProfileRules.find((profile) => profile.crateName === rule.crateName); const depsRequiringOwner = new Set(profileRule?.forbiddenNonOptionalDeps ?? []); const uncoveredDeps = new Map(); - for (const dep of deps) { + for (const dep of normalDeps) { if (!dep.optional || !depsRequiringOwner.has(dep.name) || declaredOwnerDeps.has(dep.name)) { continue; } @@ -1092,6 +1110,7 @@ export function runCoreBoundaryCheck() { escapeRegex, validateExplicitIntegrationTestTopology, agentRuntimeIntegrationTestTargets, + cliIntegrationTestTargets, }); console.log('Core boundary check self-test passed.'); return; @@ -1100,6 +1119,7 @@ export function runCoreBoundaryCheck() { checkCrateLayoutRules(); failures.push(...checkCargoDependencyBoundariesSafely({ root: ROOT, crateLayoutRules })); failures.push(...checkAgentRuntimeIntegrationTestTopology(ROOT)); + failures.push(...checkCliIntegrationTestTopology(ROOT)); for (const rule of forbiddenManifestDependencyRules) { checkForbiddenManifestDependencyRule(rule); diff --git a/scripts/core-boundaries/explicit-test-topology.mjs b/scripts/core-boundaries/explicit-test-topology.mjs index 010cd72431..e43a0659c3 100644 --- a/scripts/core-boundaries/explicit-test-topology.mjs +++ b/scripts/core-boundaries/explicit-test-topology.mjs @@ -9,6 +9,12 @@ export const agentRuntimeIntegrationTestTargets = [ { name: 'native_hook_execution_contracts', path: 'tests/native_hook_execution_contracts.rs' }, ]; +export const cliIntegrationTestTargets = [ + { name: 'acp_stdio_cli', path: 'tests/acp_stdio_cli.rs' }, + { name: 'cli_command_contracts', path: 'tests/cli_command_contracts.rs' }, + { name: 'terminal_process_contracts', path: 'tests/terminal_process_contracts.rs' }, +]; + function parseExplicitTestTargets(manifestText) { const targets = []; let current = null; @@ -141,24 +147,33 @@ export function validateExplicitIntegrationTestTopology({ return errors; } -function collectRustFiles(dir, testsDir, files) { +function collectRustFiles(dir, testsDir, files, ignoredDirectories) { for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isDirectory()) { - collectRustFiles(path, testsDir, files); + const repoPath = `tests/${relative(testsDir, path).replaceAll('\\', '/')}`; + if (!ignoredDirectories.has(repoPath)) { + collectRustFiles(path, testsDir, files, ignoredDirectories); + } } else if (entry.isFile() && entry.name.endsWith('.rs')) { - files.push(`tests/${relative(testsDir, path).replaceAll('\\', '/')}`); + const repoPath = `tests/${relative(testsDir, path).replaceAll('\\', '/')}`; + files.push(repoPath); } } } -export function checkAgentRuntimeIntegrationTestTopology(root) { - const crateDir = join(root, 'src', 'crates', 'execution', 'agent-runtime'); +function checkExplicitIntegrationTestTopology(root, { + cratePath, + expectedTargets, + ignoredDirectories = [], +}) { + const crateDir = join(root, ...cratePath.split('/')); const testsDir = join(crateDir, 'tests'); const manifestPath = join(crateDir, 'Cargo.toml'); const topLevelRustFiles = []; const leafRustFiles = []; const rootSources = new Map(); + const ignoredDirectorySet = new Set(ignoredDirectories); for (const entry of readdirSync(testsDir, { withFileTypes: true })) { if (entry.isFile() && entry.name.endsWith('.rs')) { @@ -166,15 +181,38 @@ export function checkAgentRuntimeIntegrationTestTopology(root) { topLevelRustFiles.push(repoPath); rootSources.set(repoPath, readFileSync(join(testsDir, entry.name), 'utf8')); } else if (entry.isDirectory()) { - collectRustFiles(join(testsDir, entry.name), testsDir, leafRustFiles); + const repoPath = `tests/${entry.name}`; + if (!ignoredDirectorySet.has(repoPath)) { + collectRustFiles( + join(testsDir, entry.name), + testsDir, + leafRustFiles, + ignoredDirectorySet, + ); + } } } return validateExplicitIntegrationTestTopology({ manifestText: readFileSync(manifestPath, 'utf8'), - expectedTargets: agentRuntimeIntegrationTestTargets, + expectedTargets, topLevelRustFiles, rootSources, leafRustFiles, }).map((message) => ({ path: manifestPath, line: 1, message })); } + +export function checkAgentRuntimeIntegrationTestTopology(root) { + return checkExplicitIntegrationTestTopology(root, { + cratePath: 'src/crates/execution/agent-runtime', + expectedTargets: agentRuntimeIntegrationTestTargets, + }); +} + +export function checkCliIntegrationTestTopology(root) { + return checkExplicitIntegrationTestTopology(root, { + cratePath: 'src/apps/cli', + expectedTargets: cliIntegrationTestTargets, + ignoredDirectories: ['tests/support'], + }); +} diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 076890eb3b..1813a50301 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -528,6 +528,29 @@ export const requiredContentRules = [ regex: /\brequire_capability\b/, message: 'missing typed capability requirement check', }, + { + regex: + /#\[cfg\(any\(test, feature = "test-support"\)\)\]\s*pub mod test_support;/, + message: 'runtime-services test support must stay out of ordinary library builds', + }, + { + regex: /#\[cfg\(test\)\]\s*mod runtime_services_contracts;/, + message: 'runtime-services owner contracts must run in the default crate test target', + }, + ], + }, + { + path: 'src/crates/execution/runtime-services/Cargo.toml', + reason: 'runtime-services test support must require an explicit dev-only feature', + patterns: [ + { + regex: /^test-support\s*=\s*\[\]\s*$/m, + message: 'missing empty runtime-services test-support feature', + }, + { + regex: /^default\s*=\s*\[\]\s*$/m, + message: 'runtime-services default feature set must stay empty', + }, ], }, { @@ -562,7 +585,7 @@ export const requiredContentRules = [ ], }, { - path: 'src/crates/execution/runtime-services/tests/runtime_services_contracts.rs', + path: 'src/crates/execution/runtime-services/src/runtime_services_contracts.rs', reason: 'runtime-services must keep behavior-equivalence contracts for required services, optional capabilities, registry assembly, and remote port exposure', patterns: [ diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 2ae6fd33d9..88cb9b083d 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -2344,7 +2344,7 @@ export function runManifestParserSelfTest({ contracts: ['project_agentic_frontend_event', 'projected.event_name.as_str()'], }, { - path: 'src/crates/execution/runtime-services/tests/runtime_services_contracts.rs', + path: 'src/crates/execution/runtime-services/src/runtime_services_contracts.rs', contracts: [ 'builder_requires_mandatory_runtime_services', 'fake_provider_registers_required_and_remote_services_through_registry', diff --git a/src/apps/cli/Cargo.toml b/src/apps/cli/Cargo.toml index 3717fc5b54..5e76d7b063 100644 --- a/src/apps/cli/Cargo.toml +++ b/src/apps/cli/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true description = "BitFun CLI - Terminal User Interface" default-run = "bitfun" autobins = false +autotests = false [[bin]] name = "bitfun" @@ -15,6 +16,18 @@ path = "src/main.rs" name = "bitfun-cli" path = "src/bin/bitfun_cli_compat.rs" +[[test]] +name = "acp_stdio_cli" +path = "tests/acp_stdio_cli.rs" + +[[test]] +name = "cli_command_contracts" +path = "tests/cli_command_contracts.rs" + +[[test]] +name = "terminal_process_contracts" +path = "tests/terminal_process_contracts.rs" + [dependencies] # Internal crates bitfun-core = { path = "../../crates/assembly/core", default-features = false, features = ["product-full"] } diff --git a/src/apps/cli/tests/cli_command_contracts.rs b/src/apps/cli/tests/cli_command_contracts.rs new file mode 100644 index 0000000000..dad9870ac2 --- /dev/null +++ b/src/apps/cli/tests/cli_command_contracts.rs @@ -0,0 +1,10 @@ +//! CLI command and product-assembly contract tests. + +#[path = "cli_command_contracts/compat_entrypoint.rs"] +mod compat_entrypoint; +#[path = "cli_command_contracts/exec_cli_contracts.rs"] +mod exec_cli_contracts; +#[path = "cli_command_contracts/plugin_source_cli.rs"] +mod plugin_source_cli; +#[path = "cli_command_contracts/product_assembly_cli.rs"] +mod product_assembly_cli; diff --git a/src/apps/cli/tests/compat_entrypoint.rs b/src/apps/cli/tests/cli_command_contracts/compat_entrypoint.rs similarity index 100% rename from src/apps/cli/tests/compat_entrypoint.rs rename to src/apps/cli/tests/cli_command_contracts/compat_entrypoint.rs diff --git a/src/apps/cli/tests/exec_cli_contracts.rs b/src/apps/cli/tests/cli_command_contracts/exec_cli_contracts.rs similarity index 99% rename from src/apps/cli/tests/exec_cli_contracts.rs rename to src/apps/cli/tests/cli_command_contracts/exec_cli_contracts.rs index 4fcc6ce56d..48862de237 100644 --- a/src/apps/cli/tests/exec_cli_contracts.rs +++ b/src/apps/cli/tests/cli_command_contracts/exec_cli_contracts.rs @@ -1,3 +1,4 @@ +#[path = "../support/mod.rs"] mod support; use std::process::{Command, Output}; diff --git a/src/apps/cli/tests/plugin_source_cli.rs b/src/apps/cli/tests/cli_command_contracts/plugin_source_cli.rs similarity index 100% rename from src/apps/cli/tests/plugin_source_cli.rs rename to src/apps/cli/tests/cli_command_contracts/plugin_source_cli.rs diff --git a/src/apps/cli/tests/product_assembly_cli.rs b/src/apps/cli/tests/cli_command_contracts/product_assembly_cli.rs similarity index 86% rename from src/apps/cli/tests/product_assembly_cli.rs rename to src/apps/cli/tests/cli_command_contracts/product_assembly_cli.rs index 8ec2278540..a225bdebf1 100644 --- a/src/apps/cli/tests/product_assembly_cli.rs +++ b/src/apps/cli/tests/cli_command_contracts/product_assembly_cli.rs @@ -141,14 +141,14 @@ fn doctor_rejects_incomplete_e2e_storage_roots() { #[test] fn remaining_cli_local_persistence_stays_behind_explicit_owner_boundaries() { - const ACCOUNT_SYNC: &str = include_str!("../src/account_sync.rs"); - const STARTUP_PAGE: &str = include_str!("../src/ui/startup.rs"); - const PEER_BOOTSTRAP: &str = include_str!("../src/peer_host/bootstrap.rs"); - const PEER_STATE: &str = include_str!("../src/peer_host/state.rs"); - const PEER_SESSION_COMMANDS: &str = include_str!("../src/peer_host/commands/session.rs"); - const PEER_SNAPSHOT_COMMANDS: &str = include_str!("../src/peer_host/commands/snapshot.rs"); + const ACCOUNT_SYNC: &str = include_str!("../../src/account_sync.rs"); + const STARTUP_PAGE: &str = include_str!("../../src/ui/startup.rs"); + const PEER_BOOTSTRAP: &str = include_str!("../../src/peer_host/bootstrap.rs"); + const PEER_STATE: &str = include_str!("../../src/peer_host/state.rs"); + const PEER_SESSION_COMMANDS: &str = include_str!("../../src/peer_host/commands/session.rs"); + const PEER_SNAPSHOT_COMMANDS: &str = include_str!("../../src/peer_host/commands/snapshot.rs"); const CORE_RUNTIME_SERVICES: &str = - include_str!("../../../crates/assembly/core/src/product_runtime/runtime_services.rs"); + include_str!("../../../../crates/assembly/core/src/product_runtime/runtime_services.rs"); for (path, source) in [ ("account_sync.rs", ACCOUNT_SYNC), @@ -202,10 +202,10 @@ fn remaining_cli_local_persistence_stays_behind_explicit_owner_boundaries() { #[test] fn peer_session_control_and_usage_persistence_use_runtime_sdk() { - const PEER_SESSION_COMMANDS: &str = include_str!("../src/peer_host/commands/session.rs"); - const CHAT_SELECTION: &str = include_str!("../src/modes/chat/selection.rs"); + const PEER_SESSION_COMMANDS: &str = include_str!("../../src/peer_host/commands/session.rs"); + const CHAT_SELECTION: &str = include_str!("../../src/modes/chat/selection.rs"); const CORE_PRODUCT_RUNTIME: &str = - include_str!("../../../crates/assembly/core/src/product_runtime.rs"); + include_str!("../../../../crates/assembly/core/src/product_runtime.rs"); for sdk_operation in [ "create_session_with_id", @@ -245,9 +245,9 @@ fn peer_session_control_and_usage_persistence_use_runtime_sdk() { #[test] fn local_workspace_snapshot_port_does_not_expand_the_agent_runtime_sdk() { - const RUNTIME_SDK: &str = include_str!("../../../crates/execution/agent-runtime/src/sdk.rs"); + const RUNTIME_SDK: &str = include_str!("../../../../crates/execution/agent-runtime/src/sdk.rs"); const LOCAL_SNAPSHOT_PORT: &str = - include_str!("../../../crates/contracts/runtime-ports/src/local_workspace_snapshot.rs"); + include_str!("../../../../crates/contracts/runtime-ports/src/local_workspace_snapshot.rs"); assert!(!RUNTIME_SDK.contains("LocalWorkspaceSnapshot")); assert!(!LOCAL_SNAPSHOT_PORT.contains("remote_connection_id")); @@ -258,8 +258,8 @@ fn local_workspace_snapshot_port_does_not_expand_the_agent_runtime_sdk() { #[test] fn primary_cli_session_client_uses_only_the_runtime_sdk_boundary() { - const AGENT_MODULE: &str = include_str!("../src/agent/mod.rs"); - const PRIMARY_CLIENT: &str = include_str!("../src/agent/runtime_client.rs"); + const AGENT_MODULE: &str = include_str!("../../src/agent/mod.rs"); + const PRIMARY_CLIENT: &str = include_str!("../../src/agent/runtime_client.rs"); assert!( !AGENT_MODULE.contains("trait Agent"), @@ -285,9 +285,9 @@ fn primary_cli_session_client_uses_only_the_runtime_sdk_boundary() { #[test] fn chat_context_reload_keeps_deployment_choice_behind_a_cli_adapter() { - const CHAT_MODE: &str = include_str!("../src/modes/chat.rs"); - const CHAT_CAPABILITIES: &str = include_str!("../src/modes/chat/capabilities.rs"); - const RELOAD_CLIENT: &str = include_str!("../src/agent/context_reload_client.rs"); + const CHAT_MODE: &str = include_str!("../../src/modes/chat.rs"); + const CHAT_CAPABILITIES: &str = include_str!("../../src/modes/chat/capabilities.rs"); + const RELOAD_CLIENT: &str = include_str!("../../src/agent/context_reload_client.rs"); assert!( CHAT_MODE.contains("context_reload: CliContextReloadClient"), @@ -309,7 +309,7 @@ fn chat_context_reload_keeps_deployment_choice_behind_a_cli_adapter() { #[test] fn primary_cli_runtime_client_covers_interactive_permission_and_local_turn_operations() { - const PRIMARY_CLIENT: &str = include_str!("../src/agent/runtime_client.rs"); + const PRIMARY_CLIENT: &str = include_str!("../../src/agent/runtime_client.rs"); for sdk_operation in [ "subscribe_permission_requests", @@ -326,16 +326,16 @@ fn primary_cli_runtime_client_covers_interactive_permission_and_local_turn_opera #[test] fn interactive_tui_agent_operations_stay_behind_cli_runtime_client() { - const STARTUP_PAGE: &str = include_str!("../src/ui/startup.rs"); - const CHAT_MODE: &str = include_str!("../src/modes/chat.rs"); - const CHAT_RUN: &str = include_str!("../src/modes/chat/run.rs"); - const CHAT_COMMANDS: &str = include_str!("../src/modes/chat/commands.rs"); - const CHAT_INPUT: &str = include_str!("../src/modes/chat/input.rs"); - const CHAT_SELECTION: &str = include_str!("../src/modes/chat/selection.rs"); - const RUNTIME_CLIENT: &str = include_str!("../src/agent/runtime_client.rs"); - const SHARED_RUNTIME: &str = include_str!("../src/shared_runtime.rs"); - const CLI_MAIN: &str = include_str!("../src/main.rs"); - const CLI_CARGO: &str = include_str!("../Cargo.toml"); + const STARTUP_PAGE: &str = include_str!("../../src/ui/startup.rs"); + const CHAT_MODE: &str = include_str!("../../src/modes/chat.rs"); + const CHAT_RUN: &str = include_str!("../../src/modes/chat/run.rs"); + const CHAT_COMMANDS: &str = include_str!("../../src/modes/chat/commands.rs"); + const CHAT_INPUT: &str = include_str!("../../src/modes/chat/input.rs"); + const CHAT_SELECTION: &str = include_str!("../../src/modes/chat/selection.rs"); + const RUNTIME_CLIENT: &str = include_str!("../../src/agent/runtime_client.rs"); + const SHARED_RUNTIME: &str = include_str!("../../src/shared_runtime.rs"); + const CLI_MAIN: &str = include_str!("../../src/main.rs"); + const CLI_CARGO: &str = include_str!("../../Cargo.toml"); assert!( !STARTUP_PAGE.contains("bitfun_agent_runtime::sdk::AgentRuntime"), @@ -409,10 +409,10 @@ fn interactive_tui_agent_operations_stay_behind_cli_runtime_client() { #[test] fn runtime_ownership_policy_is_assembled_once_in_core() { - const SHARED_RUNTIME: &str = include_str!("../src/shared_runtime.rs"); - const CLI_RUNTIME: &str = include_str!("../src/runtime/mod.rs"); - const CLI_MAIN: &str = include_str!("../src/main.rs"); - const AGENTIC_SYSTEM: &str = include_str!("../src/agent/agentic_system.rs"); + const SHARED_RUNTIME: &str = include_str!("../../src/shared_runtime.rs"); + const CLI_RUNTIME: &str = include_str!("../../src/runtime/mod.rs"); + const CLI_MAIN: &str = include_str!("../../src/main.rs"); + const AGENTIC_SYSTEM: &str = include_str!("../../src/agent/agentic_system.rs"); for private_policy in [ "RuntimeOwnershipKey::for_workspace", diff --git a/src/crates/assembly/core/Cargo.toml b/src/crates/assembly/core/Cargo.toml index 99266beb9a..970eb0323c 100644 --- a/src/crates/assembly/core/Cargo.toml +++ b/src/crates/assembly/core/Cargo.toml @@ -245,6 +245,7 @@ ssh-remote = [ ] [dev-dependencies] +bitfun-runtime-services = { path = "../../execution/runtime-services", features = ["test-support"] } tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } diff --git a/src/crates/assembly/product-capabilities/Cargo.toml b/src/crates/assembly/product-capabilities/Cargo.toml index 3e32fcb59c..4d8e17738e 100644 --- a/src/crates/assembly/product-capabilities/Cargo.toml +++ b/src/crates/assembly/product-capabilities/Cargo.toml @@ -18,6 +18,7 @@ bitfun-tool-packs = { path = "../../execution/tool-provider-groups", default-fea [dev-dependencies] async-trait = { workspace = true } bitfun-agent-runtime = { path = "../../execution/agent-runtime" } +bitfun-runtime-services = { path = "../../execution/runtime-services", features = ["test-support"] } tokio = { workspace = true, features = ["macros", "rt"] } [lints] diff --git a/src/crates/execution/agent-runtime/Cargo.toml b/src/crates/execution/agent-runtime/Cargo.toml index f00acf416e..cb06d24f71 100644 --- a/src/crates/execution/agent-runtime/Cargo.toml +++ b/src/crates/execution/agent-runtime/Cargo.toml @@ -36,6 +36,7 @@ tokio = { workspace = true, features = ["io-util", "macros", "process", "rt", "s tokio-util = { workspace = true } [dev-dependencies] +bitfun-runtime-services = { path = "../runtime-services", features = ["test-support"] } tokio = { workspace = true, features = ["rt-multi-thread"] } [[test]] diff --git a/src/crates/execution/runtime-services/Cargo.toml b/src/crates/execution/runtime-services/Cargo.toml index b94728d281..e9f7f0a1cf 100644 --- a/src/crates/execution/runtime-services/Cargo.toml +++ b/src/crates/execution/runtime-services/Cargo.toml @@ -9,6 +9,10 @@ description = "Typed runtime service assembly for BitFun runtimes" name = "bitfun_runtime_services" crate-type = ["rlib"] +[features] +default = [] +test-support = [] + [dependencies] bitfun-events = { path = "../../contracts/events" } bitfun-runtime-ports = { path = "../../contracts/runtime-ports" } diff --git a/src/crates/execution/runtime-services/src/lib.rs b/src/crates/execution/runtime-services/src/lib.rs index 5b9c9476ca..644fcffc8e 100644 --- a/src/crates/execution/runtime-services/src/lib.rs +++ b/src/crates/execution/runtime-services/src/lib.rs @@ -10,6 +10,9 @@ use bitfun_runtime_ports::{ }; pub mod backend_events; +#[cfg(test)] +mod runtime_services_contracts; +#[cfg(any(test, feature = "test-support"))] pub mod test_support; #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] diff --git a/src/crates/execution/runtime-services/tests/runtime_services_contracts.rs b/src/crates/execution/runtime-services/src/runtime_services_contracts.rs similarity index 98% rename from src/crates/execution/runtime-services/tests/runtime_services_contracts.rs rename to src/crates/execution/runtime-services/src/runtime_services_contracts.rs index ba2a2870a4..1fa8019c2f 100644 --- a/src/crates/execution/runtime-services/tests/runtime_services_contracts.rs +++ b/src/crates/execution/runtime-services/src/runtime_services_contracts.rs @@ -1,14 +1,14 @@ use std::sync::Arc; +use crate::test_support::{FakeRuntimePort, FakeRuntimeServicesProvider}; +use crate::{ + CapabilityAvailability, RuntimeServiceMarkerPort, RuntimeServicesBuilder, RuntimeServicesError, + RuntimeServicesProvider, RuntimeServicesRegistry, +}; use bitfun_runtime_ports::FileSystemPort; use bitfun_runtime_ports::{ RemoteWorkspaceKind, RuntimeServiceCapability, SessionStorageKind, SessionStoragePathRequest, }; -use bitfun_runtime_services::test_support::{FakeRuntimePort, FakeRuntimeServicesProvider}; -use bitfun_runtime_services::{ - CapabilityAvailability, RuntimeServiceMarkerPort, RuntimeServicesBuilder, RuntimeServicesError, - RuntimeServicesProvider, RuntimeServicesRegistry, -}; #[test] fn builder_requires_mandatory_runtime_services() {