Forráskód Böngészése

chore(bugs): log BUG-2026-07-27T120657 (spawned-task i18n fallback)

investigate-bug + diagnose-root (4-phase RCA) complete for the
thread-local set_locale bug. Bug file + registry entry + state.yaml
(active_flow: fix_bug, bug_cycle.current_step: 3) checkpointed
before kickoff-branch.
Alexander Korolev 2 hónapja
szülő
commit
ab699436f0

+ 118 - 0
specs/bugs/BUG-2026-07-27T120657-spawned-tasks-english.md

@@ -0,0 +1,118 @@
+---
+bug_id: BUG-2026-07-27T120657
+status: open
+severity: medium
+scope: i18n
+title: Spawned tasks fall back to English (thread-local set_locale)
+---
+
+# BUG-2026-07-27T120657: Spawned tasks fall back to English (thread-local set_locale)
+
+## Problem
+
+**Actual:** When the user's locale is non-English (e.g. `LANG=ru_RU.UTF-8`),
+messages emitted from inside `tokio::spawn` worker tasks render in English
+(the fallback), not the user's locale.
+
+**Expected:** All user-facing messages — including those from spawned
+parallel tasks (serve, before_serve, after_stop, power, health, etc.) —
+render in the locale set via `LANG`.
+
+**Reproduce:**
+```
+LANG=ru_RU.UTF-8 cargo run -- -d serve --recipe test-composed
+# → "[dry-run] would build composed model dir ... 3 sources, 1 file"
+#   (English; should be Russian: "3 источника, 1 файл")
+```
+
+Main-thread `t!` calls DO render in the user's locale (verified: a
+main-thread `t!("composed-dir-built", ...)` with `set_locale("ru-RU")`
+prints "3 источника, 1 файл"). Only spawned tasks are affected.
+
+## Root Cause Analysis
+
+**Code path:** `main.rs` calls `fluent_i18n::set_locale(locale)` during
+startup. `t!` macro calls read the current locale. Spawned tasks call
+`t!` from worker threads.
+
+**Why it fails:** `fluent_i18n::set_locale` writes to a `thread_local!`
+`RefCell<Option<LanguageIdentifier>>` (confirmed by reading the crate
+source: `CURRENT_LOCALE: RefCell<Option<LanguageIdentifier>>`).
+`tokio::spawn` schedules the task onto a worker thread that has its own,
+independent thread-local slot — which was never set. The `t!` macro reads
+`None` there, falls back to the static fallback locale (en-US).
+
+**Contributing factor:** The launcher uses `tokio::spawn` for parallel
+per-node SSH/Docker work across 16 call sites. Every `t!` call inside
+those spawned blocks is affected.
+
+**Risk level:** Medium. No data loss, no security boundary. User-facing
+i18n is broken for non-English locales in the async paths; English
+(the fallback and most common case) is unaffected. 16 spawn sites need
+migration.
+
+**diagnose-root 4-phase RCA (performed earlier this session):**
+- **Reproduce:** `LANG=ru_RU.UTF-8 ./sm121 -d serve --recipe test-composed`
+  → English output from spawned composed-dir builder.
+- **Isolate:** `set_locale("ru-RU")` succeeds on main;
+  `get_locale()` returns `ru-RU` on main. A `t!` call on the main thread
+  returns Russian. A `t!` call inside `tokio::spawn` returns English.
+- **Hypothesize:** `fluent_i18n::set_locale` is thread-local; tokio
+  worker threads don't inherit the main-thread locale.
+- **Verify:** Read `fluent-i18n` source — `CURRENT_LOCALE` is declared
+  `thread_local! { RefCell<Option<LanguageIdentifier>> }`. Confirmed.
+
+## TDD Fix Plan
+
+**Fix approach:** Wrap `tokio::spawn` with a locale-aware spawner that
+captures the locale from a process-global store (set once on main) and
+calls `set_locale` at the start of each spawned task. Migrate the 16
+`tokio::spawn` call sites to the wrapper.
+
+### RED-GREEN cycles
+
+1. **RED:** Write a test that sets the global locale to `ru-RU`, spawns a
+   task via the (not-yet-existing) `spawn_with_locale` helper, calls
+   `t!("composed-dir-built", ...)`, and asserts the result contains a
+   Russian word (e.g. `источник`). Test fails to compile (helper missing).
+   **GREEN:** Create `src/locale.rs` with a `OnceLock<Option<String>>`
+   global + `set_global_locale()` + `spawn_with_locale()` wrapper that
+   calls `fluent_i18n::set_locale` before awaiting the future.
+   **verify:** `cargo test -- spawn_with_locale_inherits_locale`
+
+2. **RED:** Add a characterization test proving a bare `tokio::spawn` (no
+   wrapper) still returns English despite the global locale being set —
+   documents the underlying crate behavior we're working around.
+   **GREEN:** No code change (characterization test; pins the bug).
+   **verify:** `cargo test -- bare_spawn_falls_back_to_english`
+
+3. **REFACTOR:** Migrate the 16 `tokio::spawn(async move { ... })` call
+   sites to `locale::spawn_with_locale(async move { ... })`. Mechanical
+   replacement; each site's `JoinHandle` return type is unchanged.
+   **verify:** `make check` (fmt + clippy + 128+ tests + ≤500 lines)
+
+### Migration sites (16)
+
+```
+src/tmux/ops.rs:203       src/tmux/serve.rs:108
+src/before_serve.rs:51    src/cluster_state.rs:49
+src/doctor.rs:74          src/power.rs:66, :301
+src/after_stop.rs:42      src/health.rs:64
+src/exec.rs:77            src/docker/logs.rs:77
+src/docker/mod.rs:367     src/docker/deploy.rs:47
+src/docker/health.rs:31, :77, :224
+```
+
+## Acceptance Criteria
+
+- [ ] `cargo test -- spawn_with_locale_inherits_locale` passes
+- [ ] `cargo test -- bare_spawn_falls_back_to_english` passes (characterization)
+- [ ] `make check` green (fmt + clippy + all tests + ≤500 lines)
+- [ ] `LANG=ru_RU.UTF-8 cargo run -- -d serve --recipe test-composed` prints
+      Russian for the composed-dir dry-run message
+- [ ] All 16 `tokio::spawn` sites migrated to `spawn_with_locale`
+- [ ] No new `#[allow(...)]` attributes
+
+## Resolution
+
+<!-- filled in by validate-fix -->

+ 11 - 0
specs/bugs/registry.yaml

@@ -0,0 +1,11 @@
+# sm121-launcher — bug registry
+# One row per bug. Append in chronological order. See specs/bugs/BUG-*.md for detail.
+
+- bug_id: BUG-2026-07-27T120657
+  date: 2026-07-27
+  severity: medium
+  priority: normal
+  scope: i18n
+  summary: "Spawned tasks fall back to English (thread-local set_locale)"
+  file: specs/bugs/BUG-2026-07-27T120657-spawned-tasks-english.md
+  status: open

+ 10 - 5
specs/state.yaml

@@ -5,10 +5,15 @@
 
 workflow_mode: solo-git          # solo-git: land via scripts/land-branch.sh; team-pr: gh pr
 
-active_flow: null                # null = no active epic/bug flow (Sustain phase)
+active_flow: fix_bug            # fix_bug flow: investigate → diagnose → tdd → validate → release
 active_epic_id: null
 active_story_id: null
 
+bug_cycle:
+  current_step: 3              # steps 1-2 done (investigate + diagnose); next: develop-tdd
+  completed_steps: [1, 2]     # investigate-bug + diagnose-root (4-phase RCA verified)
+  bug_id: BUG-2026-07-27T120657
+
 session:
   goal: "bigpowers onboarding complete; ready for next task"
   phase: Sustain                  # PMBOK phase: Discover|Design|Plan|Initiate|Execute|Verify|Bug|Review|Integrate|Sustain
@@ -16,12 +21,12 @@ session:
 
 git:
   branch: main
-  head: 283d2fa
-  clean: true                    # working tree clean before onboarding artifacts
+  head: 3379f83
+  clean: true                    # working tree clean; onboarding committed + pushed
 
 handoff:
-  next_skill: survey-context     # run survey-context to read this state + recommend next step
-  note: "Onboarding done. Next task triggers a flow (build_epic / fix_bug / plan-release)."
+  next_skill: kickoff-branch    # create fix branch, then develop-tdd against the bug file
+  note: "BUG-2026-07-27T120657 RCA verified. Fix branch next, then develop-tdd."
 
 metrics:
   story_start: null              # ISO 8601 at story start — informational progress marker only