Skip to content

Commit 0e99898

Browse files
authored
chore: release 2026.10425.10151
chore: release 2026.10425.10151
1 parent 8eefd60 commit 0e99898

72 files changed

Lines changed: 3062 additions & 3339 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ jobs:
8989
with:
9090
cache-key: ci-packaging-smoke
9191

92+
- name: Build release binaries (for packaging smoke)
93+
run: cargo build --release -p tnmsc -p tnmsm
94+
9295
- name: CLI packaging smoke
9396
run: cargo test -p tnmsc-integrate-tests packaging_smoke_covers_release_binary_and_global_install -- --exact --nocapture
9497

Cargo.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ members = [
2929
]
3030

3131
[workspace.package]
32-
version = "2026.10424.111"
32+
version = "2026.10425.10151"
3333
edition = "2024"
3434
rust-version = "1.88"
3535
license = "AGPL-3.0-only"

cli/local-tests/src/lib.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::fs;
44
use std::path::{Path, PathBuf};
55
use std::process::{Command, Output};
66
use std::sync::{Mutex, OnceLock};
7+
use std::time::Duration;
78

89
static BINARY_BUILT: OnceLock<()> = OnceLock::new();
910
static PROJECT_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
@@ -38,18 +39,21 @@ pub struct LocalTestRunner {
3839
binary: PathBuf,
3940
cwd: PathBuf,
4041
_lock_guard: std::sync::MutexGuard<'static, ()>,
42+
_file_lock: CrossProcessLock,
4143
}
4244

4345
impl LocalTestRunner {
4446
/// 默认在 ~/workspace/memory-sync/ 下运行测试。
4547
/// 若该目录不存在,则回退到当前目录。
4648
pub fn new() -> Self {
4749
ensure_binary();
48-
// 所有测试共享同一个真实项目目录,必须串行执行
50+
// Cross-process lock: serialises test binaries sharing the same project
51+
let file_lock = acquire_cross_process_lock();
52+
// In-process lock: serialises tests within a single binary
4953
let guard = PROJECT_LOCK
5054
.get_or_init(|| Mutex::new(()))
5155
.lock()
52-
.expect("project lock should not be poisoned");
56+
.unwrap_or_else(|e| e.into_inner());
5357
let default_project = home_dir().join("workspace").join("memory-sync");
5458
let cwd = if default_project.is_dir() {
5559
default_project
@@ -60,15 +64,17 @@ impl LocalTestRunner {
6064
binary: binary_path(),
6165
cwd,
6266
_lock_guard: guard,
67+
_file_lock: file_lock,
6368
}
6469
}
6570

6671
pub fn with_cwd(cwd: impl AsRef<Path>) -> Self {
6772
ensure_binary();
73+
let file_lock = acquire_cross_process_lock();
6874
let guard = PROJECT_LOCK
6975
.get_or_init(|| Mutex::new(()))
7076
.lock()
71-
.expect("project lock should not be poisoned");
77+
.unwrap_or_else(|e| e.into_inner());
7278
let cwd = cwd.as_ref().to_path_buf();
7379
assert!(
7480
cwd.is_dir(),
@@ -79,6 +85,7 @@ impl LocalTestRunner {
7985
binary: binary_path(),
8086
cwd,
8187
_lock_guard: guard,
88+
_file_lock: file_lock,
8289
}
8390
}
8491

@@ -126,6 +133,21 @@ impl LocalTestRunner {
126133
command_output(&mut cmd, &format!("tnmsc {}", args.join(" ")))
127134
}
128135

136+
/// 在指定目录下运行 tnmsc 命令,并设置额外环境变量。
137+
pub fn run_at_with_env(
138+
&self,
139+
cwd: impl AsRef<Path>,
140+
args: &[&str],
141+
envs: &[(&str, &str)],
142+
) -> CommandResult {
143+
let mut cmd = Command::new(&self.binary);
144+
cmd.args(args).current_dir(cwd.as_ref());
145+
for (k, v) in envs {
146+
cmd.env(k, v);
147+
}
148+
command_output(&mut cmd, &format!("tnmsc {}", args.join(" ")))
149+
}
150+
129151
pub fn run_success(&self, args: &[&str]) -> CommandResult {
130152
let result = self.run(args);
131153
result.assert_success(&format!("tnmsc {}", args.join(" ")));
@@ -341,6 +363,47 @@ impl LocalTestRunner {
341363
}
342364
}
343365

366+
// ---------------------------------------------------------------------------
367+
// Cross-process file lock — prevents test binaries from interfering with each
368+
// other when running local tests on the shared project directory.
369+
// ---------------------------------------------------------------------------
370+
371+
pub struct CrossProcessLock(Option<PathBuf>);
372+
373+
impl Drop for CrossProcessLock {
374+
fn drop(&mut self) {
375+
if let Some(path) = self.0.take() {
376+
let _ = std::fs::remove_file(&path);
377+
}
378+
}
379+
}
380+
381+
fn acquire_cross_process_lock() -> CrossProcessLock {
382+
let lock_path = home_dir().join(".tnmsc_local_test_lock");
383+
loop {
384+
match std::fs::File::create_new(&lock_path) {
385+
Ok(_) => return CrossProcessLock(Some(lock_path)),
386+
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
387+
// Stale-lock detection: if older than 5 minutes, remove and retry
388+
if let Ok(meta) = std::fs::metadata(&lock_path) {
389+
if let Ok(created) = meta.created() {
390+
if let Ok(elapsed) = created.elapsed() {
391+
if elapsed > Duration::from_secs(300) {
392+
let _ = std::fs::remove_file(&lock_path);
393+
continue;
394+
}
395+
}
396+
}
397+
}
398+
std::thread::sleep(Duration::from_millis(200));
399+
}
400+
Err(_) => {
401+
std::thread::sleep(Duration::from_millis(200));
402+
}
403+
}
404+
}
405+
}
406+
344407
pub fn ensure_binary() {
345408
let binary = binary_path();
346409

cli/local-tests/tests/claude_smoke.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ fn local_claude_clean_removes_all_project_files() {
101101
let runner = LocalTestRunner::new();
102102
runner.assert_project_ready();
103103

104+
let clean = runner.clean();
105+
clean.assert_success("tnmsc clean before install");
106+
104107
let install = runner.install();
105108
install.assert_success("tnmsc install before clean");
106109

cli/local-tests/tests/clean_blackbox.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ fn local_clean_removes_project_claude_md() {
2222
let runner = LocalTestRunner::new();
2323
runner.assert_project_ready();
2424

25+
// 先 clean 再 install 确保可复现
26+
let clean = runner.clean();
27+
clean.assert_success("tnmsc clean before install");
28+
2529
// 先 install 生成文件
2630
let install = runner.install();
2731
install.assert_success("tnmsc install before clean");
@@ -45,6 +49,10 @@ fn local_clean_dry_run_does_not_remove_files() {
4549
let runner = LocalTestRunner::new();
4650
runner.assert_project_ready();
4751

52+
// 先 clean 再 install 确保可复现
53+
let clean = runner.clean();
54+
clean.assert_success("tnmsc clean before install");
55+
4856
// 先 install 生成文件
4957
let install = runner.install();
5058
install.assert_success("tnmsc install before dry-run clean");
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//! Clean 可观测性测试:验证 clean 命令输出足够的可观测信息。
2+
3+
use tnmsc_local_tests::LocalTestRunner;
4+
5+
#[test]
6+
fn clean_outputs_key_spans_and_events() {
7+
let runner = LocalTestRunner::new();
8+
runner.assert_project_ready();
9+
10+
// 先 install 生成文件,再 clean
11+
let install = runner.install();
12+
install.assert_success("tnmsc install before clean");
13+
14+
let result = runner.run(&["--trace", "clean"]);
15+
result.assert_success("tnmsc --trace clean");
16+
17+
// 验证顶层事件
18+
assert!(
19+
result.stdout.contains("### Running clean"),
20+
"clean should output 'Running clean'. stdout:\n{}",
21+
result.stdout
22+
);
23+
24+
// 验证主要 Span
25+
assert!(
26+
result.stdout.contains("### cleanup.discover started"),
27+
"clean should output 'cleanup.discover' span. stdout:\n{}",
28+
result.stdout
29+
);
30+
assert!(
31+
result.stdout.contains("### cleanup.execute started"),
32+
"clean should output 'cleanup.execute' span. stdout:\n{}",
33+
result.stdout
34+
);
35+
}
36+
37+
#[test]
38+
fn clean_outputs_deletion_summary() {
39+
let runner = LocalTestRunner::new();
40+
runner.assert_project_ready();
41+
42+
// 先 install 生成文件,再 clean
43+
let install = runner.install();
44+
install.assert_success("tnmsc install before clean");
45+
46+
let result = runner.run(&["--info", "clean"]);
47+
result.assert_success("tnmsc --info clean");
48+
49+
// Info 级别应该输出删除摘要
50+
assert!(
51+
result.stdout.contains("Deleted") || result.stdout.contains("No files needed updates"),
52+
"clean should output deletion summary. stdout:\n{}",
53+
result.stdout
54+
);
55+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! Dry-run 可观测性测试:验证 dry-run 命令输出足够的可观测信息。
2+
3+
use tnmsc_local_tests::LocalTestRunner;
4+
5+
#[test]
6+
fn dry_run_outputs_key_spans_and_events() {
7+
let runner = LocalTestRunner::new();
8+
runner.assert_project_ready();
9+
10+
let result = runner.run(&["--trace", "dry-run"]);
11+
result.assert_success("tnmsc --trace dry-run");
12+
13+
// 验证顶层事件
14+
assert!(
15+
result.stdout.contains("### Running dry-run"),
16+
"dry-run should output 'Running dry-run'. stdout:\n{}",
17+
result.stdout
18+
);
19+
20+
// 验证主要 Span
21+
assert!(
22+
result.stdout.contains("### config.load started"),
23+
"dry-run should output 'config.load' span. stdout:\n{}",
24+
result.stdout
25+
);
26+
assert!(
27+
result.stdout.contains("### context.collect started"),
28+
"dry-run should output 'context.collect' span. stdout:\n{}",
29+
result.stdout
30+
);
31+
assert!(
32+
result.stdout.contains("### output.build started"),
33+
"dry-run should output 'output.build' span. stdout:\n{}",
34+
result.stdout
35+
);
36+
}
37+
38+
#[test]
39+
fn dry_run_outputs_plan_preview() {
40+
let runner = LocalTestRunner::new();
41+
runner.assert_project_ready();
42+
43+
let result = runner.run(&["--info", "dry-run"]);
44+
result.assert_success("tnmsc --info dry-run");
45+
46+
// Info 级别应该输出计划摘要
47+
assert!(
48+
result.stdout.contains("Planned") || result.stdout.contains("No files needed updates"),
49+
"dry-run should output plan summary. stdout:\n{}",
50+
result.stdout
51+
);
52+
}

0 commit comments

Comments
 (0)