Skip to content

Commit be9610f

Browse files
Shahinyanmclaude
andcommitted
fix(v0.9.2): serialize CLAUDE_CONFIG_DIR mutation in discovery tests
Windows CI runner failed v0.9.1 release because four tests in session::discovery::tests mutated CLAUDE_CONFIG_DIR concurrently and observed each other's writes. Symptom: assertion `left == right` failed left: "C:\\Users\\runneradmin\\.claude" right: "/tmp/custom-claude-config" Fix: module-level `static ENV_LOCK: Mutex<()> = Mutex::new(());` acquired at the top of every test that touches CLAUDE_CONFIG_DIR. Also: claude_config_dir_handles_env_var uses std::env::temp_dir() for a portable path instead of hardcoded /tmp/... which doesn't exist on Windows. Linux + macOS passed silently — the race existed there too but the particular scheduling order didn't trip it. The mutex prevents the race on every platform. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3cf8b71 commit be9610f

8 files changed

Lines changed: 70 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.9.2] - 2026-05-17
11+
12+
### Fixed
13+
- Windows CI flake in `session::discovery::tests::*` — four tests
14+
mutated `CLAUDE_CONFIG_DIR` in parallel and observed each other's
15+
writes. Now serialized through a module-level `Mutex<()>`; the
16+
Windows runner sees the expected override path. Linux/macOS were
17+
asymptomatically affected by the same race.
18+
- `claude_config_dir_respects_env_var` no longer hardcodes
19+
`/tmp/custom-claude-config` (invalid on Windows). Uses
20+
`std::env::temp_dir()` for a portable path.
21+
1022
## [0.9.1] - 2026-05-17
1123

1224
### Fixed

Cargo.lock

Lines changed: 3 additions & 3 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
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.9.1"
10+
version = "0.9.2"
1111
edition = "2021"
1212
rust-version = "1.88"
1313
license = "MIT"

crates/tj-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "task-journal"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
tj-core = { package = "task-journal-core", version = "0.9.1", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.9.2", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
clap = { workspace = true }
2222
tracing = { workspace = true }

crates/tj-core/src/session/discovery.rs

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ fn dirs_home() -> anyhow::Result<PathBuf> {
136136
#[cfg(test)]
137137
mod tests {
138138
use super::*;
139+
use std::sync::Mutex;
140+
141+
/// Serialize every test that touches `CLAUDE_CONFIG_DIR`. Cargo runs
142+
/// unit tests in parallel by default; two tests mutating the same
143+
/// process env race (set in A, observed in B) and flaked Windows CI
144+
/// (saw "C:\Users\runneradmin\.claude" when expecting the override).
145+
/// Tests that touch the env take this lock before the first set_var.
146+
/// `lock().unwrap_or_else(|p| p.into_inner())` swallows poisoning
147+
/// from a panicking sibling test — env is restored regardless.
148+
static ENV_LOCK: Mutex<()> = Mutex::new(());
139149

140150
#[test]
141151
fn encode_path_replaces_separators() {
@@ -257,6 +267,7 @@ mod tests {
257267

258268
#[test]
259269
fn find_project_dir_with_env_override() {
270+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
260271
let dir = tempfile::tempdir().unwrap();
261272
let projects = dir.path().join("projects");
262273
std::fs::create_dir_all(&projects).unwrap();
@@ -281,6 +292,7 @@ mod tests {
281292

282293
#[test]
283294
fn find_project_dir_returns_none_when_no_match() {
295+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
284296
let dir = tempfile::tempdir().unwrap();
285297
let projects = dir.path().join("projects");
286298
std::fs::create_dir_all(&projects).unwrap();
@@ -296,6 +308,7 @@ mod tests {
296308

297309
#[test]
298310
fn find_project_dir_returns_none_when_projects_dir_missing() {
311+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
299312
let dir = tempfile::tempdir().unwrap();
300313
// Don't create a "projects" subdir — it doesn't exist.
301314

@@ -319,20 +332,46 @@ mod tests {
319332

320333
// --- claude_config_dir ---
321334

335+
/// Both `CLAUDE_CONFIG_DIR` cases are combined into one test so the
336+
/// env-var read/write/restore steps run serially. Cargo runs unit
337+
/// tests in parallel by default; two tests touching the same process
338+
/// env on Windows raced (set in test A, observed in test B) and
339+
/// flaked CI. Save → set → assert → restore inside one body makes
340+
/// the dependency local. Uses a portable tempdir-style path rather
341+
/// than the hardcoded "/tmp/..." that doesn't exist on Windows.
322342
#[test]
323-
fn claude_config_dir_respects_env_var() {
324-
std::env::set_var("CLAUDE_CONFIG_DIR", "/tmp/custom-claude-config");
343+
fn claude_config_dir_handles_env_var() {
344+
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
345+
// SAFETY: the ENV_LOCK above serializes us against the other
346+
// CLAUDE_CONFIG_DIR tests in this module; prev → restore at
347+
// the end gives a clean exit regardless of panic.
348+
let prev = std::env::var_os("CLAUDE_CONFIG_DIR");
349+
350+
// Case 1: non-empty value is honored verbatim. Use a portable
351+
// path (std::env::temp_dir() works on Linux/macOS/Windows).
352+
let custom = std::env::temp_dir().join("tj-custom-claude-config");
353+
unsafe {
354+
std::env::set_var("CLAUDE_CONFIG_DIR", &custom);
355+
}
325356
let dir = claude_config_dir().unwrap();
326-
std::env::remove_var("CLAUDE_CONFIG_DIR");
327-
assert_eq!(dir, PathBuf::from("/tmp/custom-claude-config"));
328-
}
357+
assert_eq!(dir, custom);
329358

330-
#[test]
331-
fn claude_config_dir_ignores_empty_env_var() {
332-
std::env::set_var("CLAUDE_CONFIG_DIR", "");
359+
// Case 2: empty value falls back to home + .claude.
360+
unsafe {
361+
std::env::set_var("CLAUDE_CONFIG_DIR", "");
362+
}
333363
let dir = claude_config_dir().unwrap();
334-
std::env::remove_var("CLAUDE_CONFIG_DIR");
335-
// Should fall back to home dir + .claude.
336-
assert!(dir.to_string_lossy().ends_with(".claude"));
364+
assert!(
365+
dir.to_string_lossy().ends_with(".claude"),
366+
"fallback must land in <home>/.claude, got: {dir:?}"
367+
);
368+
369+
// Restore.
370+
unsafe {
371+
match prev {
372+
Some(v) => std::env::set_var("CLAUDE_CONFIG_DIR", v),
373+
None => std::env::remove_var("CLAUDE_CONFIG_DIR"),
374+
}
375+
}
337376
}
338377
}

crates/tj-mcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "task-journal-mcp"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
tj-core = { package = "task-journal-core", version = "0.9.1", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.9.2", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
tokio = { workspace = true }
2222
tracing = { workspace = true }

plugin/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-journal",
3-
"version": "0.9.1",
3+
"version": "0.9.2",
44
"description": "Append-only journal of AI-coding task reasoning chains: hypotheses, decisions, rejections, evidence. Renders compact resume packs so an agent can pick up a 2-week-old task with full context.",
55
"author": {
66
"name": "Mher Shahinyan"

plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-journal",
3-
"version": "0.9.1",
3+
"version": "0.9.2",
44
"description": "Append-only journal of AI-coding task reasoning chains. Captures hypotheses, decisions, rejections, evidence — renders compact resume packs so an agent can pick up a 2-week-old task with full context.",
55
"author": {
66
"name": "Mher Shahinyan",

0 commit comments

Comments
 (0)