Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/architecture/rust-build-dependency-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ DTO/contract 抽取不等于 runtime owner 迁移。迁移 owner 必须先审查
- integration test 只依赖被测 owner 的公开契约,不通过 `product-full` 获取测试便利;窄 feature 尚不能独立编译时,应将其记录为待拆分的 owner/feature 边界并保持现有 target 声明,不得新增或扩大 `product-full` 来制造已经收敛的假象;
- 纯解析、策略和状态转换优先使用无外部系统的 owner-local fixture;
- 需要真实 adapter/service 的测试单独作为 feature integration target;
- 同一 owner 内 feature、平台与依赖闭包完全相同的 integration tests,应按稳定职责收敛为少量显式 target,避免每个源文件重复编译和链接同一闭包;本地通过 `--test <target> <module>::<filter>` 保留 focused test。不同 feature、平台、进程或外部系统边界不得为减少 target 数而合并;
- 测试常用、真实的 feature 组合,不穷举指数级组合;
- `--all-features` 用于兼容审计,不代替目标产品的最小组合测试。

Expand Down
23 changes: 23 additions & 0 deletions docs/performance/01-compile-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
| F17 | pnpm 结构小问题:tests/e2e 已在 workspace 内仍单独 install;installer 独立 Rust workspace 导致 Tauri 栈本地编译两份 | 结构 | 低 | `pnpm-workspace.yaml`、`package.json:92`、`Cargo.toml:40-42` |
| F18 | build.rs 生成代码遍历 HashMap,输出字节序不确定 → 不可复现构建,削弱 sccache/远端缓存效果 | Rust | 低 | `src/crates/assembly/core/build.rs:303,429`、`src/apps/cli/build.rs` |
| F19 | `[profile.dev] incremental = true` 为默认值,冗余;dev profile 无任何针对性调优 | Rust | 低 | `Cargo.toml:279-280` |
| F20 | `bitfun-agent-runtime` 的 28 个 integration targets 已收敛为 5 个显式目标,同时保留 Unix 进程测试隔离 | Rust/Test | 已兑现 | `src/crates/execution/agent-runtime/Cargo.toml`、`tests/agent_*_contracts.rs` |

---

Expand Down Expand Up @@ -243,6 +244,27 @@ Desktop、CLI、ACP、Server 与 SDK Host 的内容、错误和生命周期完

`Cargo.toml:279-280` 的 `incremental = true` 是 dev 默认值,可删;该段落是放置 F5 建议(`debug = "line-tables-only"`)的天然位置。

### F20(已治理)agent-runtime integration test 重复链接

`bitfun-agent-runtime` 没有可选 feature,原 28 个 integration test target 中的 27 个跨平台契约使用相同的依赖闭包,
却在每次 `cargo test -p bitfun-agent-runtime` 时分别编译和链接。当前通过 `autotests = false` 将它们按定义、会话、
交互和 long-horizon 职责归入 4 个 target;另保留 1 个 Unix-only 原生进程 target,避免为了减少数量而跨平台或进程
边界合并。246 个 Unix integration tests、224 个 Windows integration tests 及原有 lib tests 均保留。现有 CI 命令和
覆盖范围不变,未新增测试、feature、依赖或 workflow;现有边界检查会拒绝未注册入口和未被引用的叶测试文件。

以下是本机观察值,不作为其他机器的固定收益承诺。测量日期 2026-08-03,基线
`53c8c029a8b6245e810cbee0707c820bc74fb7b8`,Windows 10.0.19045、i7-10700、rustc/cargo 1.97.1;依赖预热后按
原布局/现布局交错执行 A/B/A/B/A/B,每次运行
`cargo clean -p bitfun-agent-runtime` 和 `cargo test -p bitfun-agent-runtime --no-run --locked --quiet`:

| 布局 | 三次有效样本 | 中位数 | integration PDB |
|---|---|---:|---:|
| 原 28 targets | 12.48s / 15.01s / 13.63s | 13.63s | 312.3 MiB |
| 现 5 targets | 11.90s / 11.04s / 11.31s | 11.31s | 77.4 MiB |

本机包级 test 编译/链接中位数降低约 17.0%,PDB 体积降低约 75.2%。这是测试可执行目标治理,不代表第三方依赖
或 feature 数量减少;有独立 feature、平台、进程或外部系统边界的测试仍必须保持独立 target。

---

## 三、实施建议清单(可直接派发给实施 agent)
Expand All @@ -265,6 +287,7 @@ Desktop、CLI、ACP、Server 与 SDK Host 的内容、错误和生命周期完
| T12 | beforeBuildCommand 并行(F13):新增 `scripts/frontend-build-all.mjs` 并行跑 build:web 与 prepare:mobile-web,tauri.conf.json / tauri.dev.conf.json 的 beforeBuildCommand 指向它;dev.cjs 准备步骤改 Promise.all。验收:desktop:build 前端阶段时长≈max(两者) 而非 sum。 | `src/apps/desktop/tauri.conf.json`、`tauri.dev.conf.json`、`scripts/dev.cjs`、新脚本 | 低 |
| T13 | bitfun-core 拆分启动(F3,长期):先跑 `cargo build --timings` 与 `cargo tree -d` 存档基线;选 1-2 个低耦合子域(如 announcement、debug-log server)试点拆出独立 crate 并保留 re-export;结合 F12 的"dev 运行时读取提示词"改造。验收:改动试点子域后 `cargo build -p bitfun-desktop` 的重编 crate 数与耗时下降。 | `src/crates/assembly/core/**`、根 `Cargo.toml` members | 中-高(架构改动,分多个 PR 渐进) |
| T14 | 可选工具链增强(F5):提交 `.cargo/config.toml` 模板(注释形式提供 rust-lld 与 sccache 配置,默认不启用),团队自选开启;CI 冷构建可评估 sccache-action。验收:提供文档,默认行为不变。 | 新增 `.cargo/config.toml`、文档 | 低(默认关闭) |
| T15(已完成) | agent-runtime integration target 收敛(F20):保持全部 contract test 源与现有 CI 命令不变,将 27 个跨平台契约按定义、会话、交互、long-horizon 职责归为 4 个 target,Unix 原生进程测试保持独立;focused test 使用 `--test <target> <module>::<filter>`。 | `agent-runtime/Cargo.toml`、`agent-runtime/tests/**`、现有 boundary rule 路径 | 已完成;总体 28→5,只减少重复编译/链接,不改变 feature 或依赖闭包 |

### 快速收益组合(建议第一批实施)
T2 + T4 + T5 + T6 + T9 + T10 + T12:全部低风险,合计可显著改善日常 dev 循环(启动省 20-60s、增量链接提速、dev CPU 下降)与 build:web 时长;随后再做 T1(最大单项前端收益)、T3(打包 CI)、T7/T8(CI 结构)。
Expand Down
1 change: 1 addition & 0 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const ENTRYPOINT = new URL('./check-core-boundaries.mjs', import.meta.url);
const MODULES = [
'./core-boundaries/checker.mjs',
'./core-boundaries/cargo-dependency-boundaries.mjs',
'./core-boundaries/explicit-test-topology.mjs',
'./core-boundaries/manifest-feature-helpers.mjs',
'./core-boundaries/self-test.mjs',
'./core-boundaries/rules/crate-rules.mjs',
Expand Down
11 changes: 9 additions & 2 deletions scripts/core-boundaries/checker.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join, relative } from 'path';
import { dirname, join, relative } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

import {
dependencyProfileRules,
Expand Down Expand Up @@ -36,6 +35,11 @@ import {
unexpectedReachableLocalFeatures,
} from './manifest-feature-helpers.mjs';
import { checkCargoDependencyBoundariesSafely } from './cargo-dependency-boundaries.mjs';
import {
agentRuntimeIntegrationTestTargets,
checkAgentRuntimeIntegrationTestTopology,
validateExplicitIntegrationTestTopology,
} from './explicit-test-topology.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..', '..');
Expand Down Expand Up @@ -1086,13 +1090,16 @@ export function runCoreBoundaryCheck() {
hasPluginWildcardReexport,
createFacadeLineChecker,
escapeRegex,
validateExplicitIntegrationTestTopology,
agentRuntimeIntegrationTestTargets,
});
console.log('Core boundary check self-test passed.');
return;
}

checkCrateLayoutRules();
failures.push(...checkCargoDependencyBoundariesSafely({ root: ROOT, crateLayoutRules }));
failures.push(...checkAgentRuntimeIntegrationTestTopology(ROOT));

for (const rule of forbiddenManifestDependencyRules) {
checkForbiddenManifestDependencyRule(rule);
Expand Down
180 changes: 180 additions & 0 deletions scripts/core-boundaries/explicit-test-topology.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { readdirSync, readFileSync } from 'node:fs';
import { join, posix, relative } from 'node:path';

export const agentRuntimeIntegrationTestTargets = [
{ name: 'agent_definition_contracts', path: 'tests/agent_definition_contracts.rs' },
{ name: 'agent_interaction_contracts', path: 'tests/agent_interaction_contracts.rs' },
{ name: 'agent_long_horizon_contracts', path: 'tests/agent_long_horizon_contracts.rs' },
{ name: 'agent_session_contracts', path: 'tests/agent_session_contracts.rs' },
{ name: 'native_hook_execution_contracts', path: 'tests/native_hook_execution_contracts.rs' },
];

function parseExplicitTestTargets(manifestText) {
const targets = [];
let current = null;
const finishCurrent = () => {
if (current) {
targets.push(current);
current = null;
}
};

for (const line of manifestText.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed === '[[test]]') {
finishCurrent();
current = {};
continue;
}
if (trimmed.startsWith('[')) {
finishCurrent();
continue;
}
const field = current && trimmed.match(/^(name|path)\s*=\s*"([^"]+)"\s*$/);
if (field) {
current[field[1]] = field[2];
}
}
finishCurrent();
return targets;
}

function packageDisablesAutotests(manifestText) {
let inPackage = false;
for (const line of manifestText.split(/\r?\n/)) {
const trimmed = line.trim();
if (trimmed.startsWith('[')) {
inPackage = trimmed === '[package]';
continue;
}
if (inPackage && /^autotests\s*=\s*false\s*$/.test(trimmed)) {
return true;
}
}
return false;
}

function parseFlatRootModules(root, source, errors) {
const references = [];
const lines = source.split(/\r?\n/);
let valid = true;
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index].trim();
if (line === '' || line.startsWith('//!')) {
continue;
}
const pathAttribute = line.match(/^#\[path\s*=\s*"([^"]+)"\]$/);
const moduleDeclaration = lines[index + 1]?.trim().match(/^mod\s+([A-Za-z0-9_]+)\s*;$/);
if (!pathAttribute || !moduleDeclaration) {
errors.push(`grouped test root ${root} contains unsupported line ${index + 1}`);
valid = false;
continue;
}
references.push({ path: pathAttribute[1], moduleName: moduleDeclaration[1] });
index += 1;
}
return valid ? references : [];
}

export function validateExplicitIntegrationTestTopology({
manifestText,
expectedTargets,
topLevelRustFiles,
rootSources,
leafRustFiles,
}) {
const errors = [];
if (!packageDisablesAutotests(manifestText)) {
errors.push('[package] must keep autotests = false');
}

const expectedTargetEntries = expectedTargets.map(({ name, path }) => `${name}=${path}`).sort();
const actualTargetEntries = parseExplicitTestTargets(manifestText)
.map(({ name, path }) => `${name ?? '<missing-name>'}=${path ?? '<missing-path>'}`)
.sort();
if (actualTargetEntries.join('\n') !== expectedTargetEntries.join('\n')) {
errors.push(`explicit test targets must be exactly: ${expectedTargetEntries.join(', ')}`);
}

const expectedRoots = expectedTargets.map(({ path }) => path).sort();
if ([...topLevelRustFiles].sort().join('\n') !== expectedRoots.join('\n')) {
errors.push(`top-level test roots must be exactly: ${expectedRoots.join(', ')}`);
}

const leaves = new Set(leafRustFiles);
const referenceCounts = new Map();
for (const root of expectedRoots) {
const source = rootSources.get(root);
if (source === undefined) {
errors.push(`missing explicit test root: ${root}`);
continue;
}
const wrapperDir = `${root.slice(0, -'.rs'.length)}/`;
const ownsLeaves = [...leaves].some((leaf) => leaf.startsWith(wrapperDir));
if (!ownsLeaves) {
continue;
}
for (const reference of parseFlatRootModules(root, source, errors)) {
const leaf = posix.normalize(posix.join(posix.dirname(root), reference.path));
if (!leaf.startsWith(wrapperDir)) {
errors.push(`grouped test root ${root} may only reference leaves under ${wrapperDir}`);
continue;
}
if (!leaves.has(leaf)) {
errors.push(`test root ${root} references missing leaf: ${leaf}`);
continue;
}
const expectedModuleName = posix.basename(leaf, '.rs');
if (reference.moduleName !== expectedModuleName) {
errors.push(`test leaf ${leaf} must use module name ${expectedModuleName}`);
}
referenceCounts.set(leaf, (referenceCounts.get(leaf) ?? 0) + 1);
}
}

for (const leaf of [...leaves].sort()) {
const count = referenceCounts.get(leaf) ?? 0;
if (count !== 1) {
errors.push(`test leaf ${leaf} must be referenced exactly once; found ${count}`);
}
}
return errors;
}

function collectRustFiles(dir, testsDir, files) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
collectRustFiles(path, testsDir, files);
} else if (entry.isFile() && entry.name.endsWith('.rs')) {
files.push(`tests/${relative(testsDir, path).replaceAll('\\', '/')}`);
}
}
}

export function checkAgentRuntimeIntegrationTestTopology(root) {
const crateDir = join(root, 'src', 'crates', 'execution', 'agent-runtime');
const testsDir = join(crateDir, 'tests');
const manifestPath = join(crateDir, 'Cargo.toml');
const topLevelRustFiles = [];
const leafRustFiles = [];
const rootSources = new Map();

for (const entry of readdirSync(testsDir, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith('.rs')) {
const repoPath = `tests/${entry.name}`;
topLevelRustFiles.push(repoPath);
rootSources.set(repoPath, readFileSync(join(testsDir, entry.name), 'utf8'));
} else if (entry.isDirectory()) {
collectRustFiles(join(testsDir, entry.name), testsDir, leafRustFiles);
}
}

return validateExplicitIntegrationTestTopology({
manifestText: readFileSync(manifestPath, 'utf8'),
expectedTargets: agentRuntimeIntegrationTestTargets,
topLevelRustFiles,
rootSources,
leafRustFiles,
}).map((message) => ({ path: manifestPath, line: 1, message }));
}
2 changes: 1 addition & 1 deletion scripts/core-boundaries/rules/source/forbidden-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const forbiddenContentRules = [
],
},
{
path: 'src/crates/execution/agent-runtime/tests/sdk_smoke.rs',
path: 'src/crates/execution/agent-runtime/tests/agent_session_contracts/sdk_smoke.rs',
patterns: [
{
regex: /\bbitfun_runtime_services::test_support\b/,
Expand Down
Loading