wintf 0.0.3

Windows Tategaki Framework - Rust UI library with Japanese vertical text support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
//! ウィンドウメッセージ受理(`window_pos.rs`/`lifecycle.rs`)の**遷移観測**に対する
//! 決定論的テスト。
//!
//! 固定するのは 4 つである。
//!
//! - **受理の記録**——拡大率変更(`WM_DPICHANGED`)・窓位置変更(`WM_WINDOWPOSCHANGED`)・
//!   表示構成変更(`WM_DISPLAYCHANGE`)の 3 メッセージが、窓ハンドルと「窓書込の内側で
//!   受理されたか」(`in_swp`)・「一括書込の開始からの経過」(`since_flush_us`)つきで
//!   記録されること。
//! - **同期書込の段**——OS 提案位置を採用したときの書込が `stage=sync` で記録され、
//!   一括書込(`stage=flush`)と区別できること。**採否の判定自体は変えない**——同じ
//!   捕捉窓の中で既存の採否ログ(`applied=`)と並存することを見る。
//! - **既定 OFF**——観測 target を点けていない運転では 1 行も出ないこと。
//! - **前置ガード**——`WM_WINDOWPOSCHANGED` はドラッグ中も含めて絶えず走る経路であり
//!   (要件 10.6)、既定運転で行の組立・時刻読み・矩形の読み戻しを一切行わないこと。
//!   これは出力では示せない(`emit_line` の水準を subscriber が濾すため既定では
//!   どのみち無音になる)ので、本文の構造で固定する。
//!
//! # 自発書込カウンタはスレッド局所である(要件 7.7・7.1)
//!
//! `in_swp` の出所である `is_self_initiated()` は `SELF_INITIATED_DEPTH`
//! (`window/command.rs`)を読む。このカウンタは**スレッドごとに独立**であり、
//! `SetWindowPos`/`EndDeferWindowPos` が `WM_WINDOWPOSCHANGED` を呼び出しスレッドの上で
//! 同期送達することから、持ち上げ・送達・判定・解放が 1 本のスレッドの内側で閉じる。よって
//! 並列に走る別テストの `guarded_set_window_pos` が持ち上げた値はこちらから見えず、値を読む
//! テストも値を持ち上げるテストも直列化の錠を取らない。
//!
//! (共有の `AtomicI32` だった頃は見えてしまい、`in_swp=false` の検査が気まぐれに落ちた
//! ——是正前の実測: `cargo test -p wintf --lib` 60 周中 11 周が赤。スレッド局所であることの
//! 本体は `window/command_threadlocal_tests.rs`。)
//!
//! # 実行形態の明示(要件 7.6)
//!
//! 本ファイルの検証はすべて**テストスレッド上の直接呼出**である。wndproc のハンドラは
//! 呼び出しスレッドで走り、一括書込(`flush`)も同様なので、`tracing` のスレッド局所
//! subscriber([`capture_under_filter`])で確実に捕捉できる。多スレッド実行器の相での
//! 「出ないこと」は一切主張しない。
//!
//! 「出ない」を見るテストには、同じ捕捉窓の内側に必ず拾えるはずの**対照行**を置く
//! ——捕捉が死んでいるだけで緑になる形にしない。
//!
//! # 実窓を使う 1 本について
//!
//! `WM_WINDOWPOSCHANGED` の `in_swp=true`/`since_flush_us` が実際に立つのは
//! **`SetWindowPos` が同期送達したとき**だけである。これは実窓が要るが、`SetWindowPos`
//! は同一スレッドの窓へ**同期 send** するのでメッセージポンプは要らない——よって
//! 実機サインオフではなく決定論テストで再現できる(再観測 §5 の 24/24=「各窓の最初の
//! 書込の内側で当該窓のメッセージが同期処理される」に対応する形)。

use std::cell::RefCell;
use std::rc::Rc;

use bevy_ecs::prelude::Entity;
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
    CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, SET_WINDOW_POS_FLAGS,
    SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WINDOW_EX_STYLE, WINDOWPOS, WM_DISPLAYCHANGE,
    WM_DPICHANGED, WM_WINDOWPOSCHANGED, WNDCLASSEXW, WS_EX_TOOLWINDOW, WS_POPUP,
};
use windows::core::{PCWSTR, w};

use crate::ecs::App;
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::transition_diag::{
    self, KIND_MSG, KIND_WINDPI, KIND_WRITE, MISSING, MSG_DISPLAYCHANGE, MSG_DPICHANGED,
    MSG_WINDOWPOSCHANGED, ORIGIN_DPI_SUGGESTED, RECORD_PREFIX_TAG, TRANSITION_TARGET,
};
use crate::ecs::window::{DPI, DpiSuggestedRectPolicy, SetWindowPosCommand, WindowHandle};
use crate::ecs::world::EcsWorld;
use crate::executor::util::WindowMessage;

/// 実機サインオフが用いる directive のうち、本チャネルを点灯させるもの。
const SIGNOFF_DIRECTIVES: &str = "info,wintf::transition=debug";

/// 既定水準(観測チャネルを有効化していない通常運転)。
const DEFAULT_DIRECTIVES: &str = "info";

/// 捕捉が生きている証拠として同じ窓の中で必ず出す対照行。
const CONTROL_LINE: &str = "[transition-probe] alive";

/// 対照行を出す(`info` 水準ゆえ両 directive で通る)。
fn emit_control() {
    tracing::info!(target: TRANSITION_TARGET, "{CONTROL_LINE}");
}

/// テスト冒頭の初期化——キューの残り・刻印の写し・残置コンテキストを落とす(要件 7.7)。
fn clean_slate() {
    let _residue = crate::ecs::window::drain_window_pos_commands();
    let _context = crate::ecs::window::DpiChangeContext::take();
    transition_diag::reset_for_test();
}

/// 捕捉出力から観測チャネルの行だけを取り出す。
fn transition_lines(captured: &str) -> Vec<&str> {
    captured
        .lines()
        .filter(|line| line.contains(RECORD_PREFIX_TAG))
        .collect()
}

/// 種別語 `kind=<kind>` を持つ行だけを取り出す。
fn lines_of_kind<'a>(captured: &'a str, kind: &str) -> Vec<&'a str> {
    let needle = format!("kind={kind} ");
    transition_lines(captured)
        .into_iter()
        .filter(|line| line.contains(&needle))
        .collect()
}

/// 行に載るハンドル表現(`transition_diag` の `hwnd_field` と同じ書式)。
fn hwnd_field(hwnd: HWND) -> String {
    format!("hwnd=0x{:X}", hwnd.0 as usize)
}

// ---------------------------------------------------------------------------
// ヘッドレス配送(実 HWND 不要)
// ---------------------------------------------------------------------------

/// `WM_DPICHANGED` メッセージを組む(LOWORD=X DPI / HIWORD=Y DPI・LPARAM=提案矩形)。
fn dpichanged_message(hwnd: HWND, new_dpi: u16, suggested: &RECT) -> WindowMessage {
    WindowMessage {
        hwnd,
        msg: WM_DPICHANGED,
        wparam: WPARAM(((new_dpi as usize) << 16) | new_dpi as usize),
        lparam: LPARAM(suggested as *const RECT as isize),
    }
}

/// 政策 component の有無を変えて `WM_DPICHANGED` を 1 回配送し、観測出力を返す。
fn capture_dpichanged(directives: &str, policy: Option<DpiSuggestedRectPolicy>) -> String {
    clean_slate();
    let world = Rc::new(RefCell::new(EcsWorld::new()));
    let entity = {
        let mut w = world.borrow_mut();
        let mut e = w.world_mut().spawn(DPI::from_dpi(96, 96));
        if let Some(p) = policy {
            e.insert(p);
        }
        e.id()
    };

    let suggested = RECT {
        left: 3210,
        top: 140,
        right: 3810,
        bottom: 620,
    };
    let m = dpichanged_message(HWND(std::ptr::null_mut()), 192, &suggested);
    let out = capture_under_filter(directives, || {
        emit_control();
        let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
    });
    clean_slate();
    out
}

/// `WM_DPICHANGED` を 1 回配送し、観測出力と**対象エンティティ**を返す。
///
/// `with_dpi` が偽のとき、対象は `DPI` component を持たない——`WM_DPICHANGED` の
/// component 書き換えが `if let Some(mut dpi_comp)` で抜ける腕(=**書き換えが起きない
/// 入力**)である。
fn capture_dpichanged_with_entity(directives: &str, with_dpi: bool) -> (String, Entity) {
    clean_slate();
    let world = Rc::new(RefCell::new(EcsWorld::new()));
    let entity = {
        let mut w = world.borrow_mut();
        let mut e = w.world_mut().spawn(());
        if with_dpi {
            e.insert(DPI::from_dpi(96, 96));
        }
        e.id()
    };

    let suggested = RECT {
        left: 3210,
        top: 140,
        right: 3810,
        bottom: 620,
    };
    let m = dpichanged_message(HWND(std::ptr::null_mut()), 192, &suggested);
    let out = capture_under_filter(directives, || {
        emit_control();
        let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
    });
    clean_slate();
    (out, entity)
}

/// `WM_WINDOWPOSCHANGED` を 1 回配送し、観測出力を返す(窓書込の**外側**での受理)。
fn capture_windowposchanged(directives: &str) -> String {
    clean_slate();
    let world = Rc::new(RefCell::new(EcsWorld::new()));
    let entity = world.borrow_mut().world_mut().spawn(()).id();

    let windowpos = WINDOWPOS {
        hwnd: HWND(std::ptr::null_mut()),
        hwndInsertAfter: HWND(std::ptr::null_mut()),
        x: 100,
        y: 200,
        cx: 640,
        cy: 480,
        flags: SET_WINDOW_POS_FLAGS(0),
    };
    let m = WindowMessage {
        hwnd: HWND(std::ptr::null_mut()),
        msg: WM_WINDOWPOSCHANGED,
        wparam: WPARAM(0),
        lparam: LPARAM(&windowpos as *const WINDOWPOS as isize),
    };
    let out = capture_under_filter(directives, || {
        emit_control();
        let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
    });
    clean_slate();
    out
}

// ---------------------------------------------------------------------------
// メッセージ受理の記録
// ---------------------------------------------------------------------------

#[test]
fn dpichanged_receipt_is_recorded_with_handle_and_flush_context() {
    let captured = capture_dpichanged(SIGNOFF_DIRECTIVES, None);
    assert!(
        captured.contains(CONTROL_LINE),
        "対照が拾えていない=捕捉が死んでいる: {captured}"
    );

    let msgs = lines_of_kind(&captured, KIND_MSG);
    assert_eq!(msgs.len(), 1, "受理 1 回につき 1 行: {captured}");
    let line = msgs[0];
    assert!(
        line.contains(&format!("msg={MSG_DPICHANGED} ")),
        "メッセージ名が載る: {line}"
    );
    assert!(
        line.contains(&hwnd_field(HWND(std::ptr::null_mut()))),
        "対象ハンドルが載る: {line}"
    );
    assert!(
        line.contains("in_swp=false"),
        "窓書込の外側で受理したので in_swp は偽: {line}"
    );
    assert!(
        line.contains("since_flush_us=-"),
        "一括書込の外で受理したので経過は番兵: {line}"
    );
}

#[test]
fn windowposchanged_receipt_is_recorded_outside_a_window_write() {
    let captured = capture_windowposchanged(SIGNOFF_DIRECTIVES);
    assert!(
        captured.contains(CONTROL_LINE),
        "対照が拾えていない=捕捉が死んでいる: {captured}"
    );

    let msgs = lines_of_kind(&captured, KIND_MSG);
    assert_eq!(msgs.len(), 1, "受理 1 回につき 1 行: {captured}");
    assert!(
        msgs[0].contains(&format!("msg={MSG_WINDOWPOSCHANGED} ")),
        "メッセージ名が載る: {}",
        msgs[0]
    );
    assert!(
        msgs[0].contains("in_swp=false") && msgs[0].contains("since_flush_us=-"),
        "自前の窓書込を伴わない受理は両フィールドとも「外側」を示す: {}",
        msgs[0]
    );
}

#[test]
fn displaychange_receipt_is_recorded_and_still_delegates_to_defwindowproc() {
    clean_slate();
    let world = Rc::new(RefCell::new(EcsWorld::new()));
    let entity = {
        let mut w = world.borrow_mut();
        w.world_mut().insert_resource(App::new());
        w.world_mut().spawn(()).id()
    };

    let m = WindowMessage {
        hwnd: HWND(0x4321 as *mut _),
        msg: WM_DISPLAYCHANGE,
        wparam: WPARAM(0),
        lparam: LPARAM(0),
    };
    let mut ret: Option<LRESULT> = Some(LRESULT(1));
    let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
        emit_control();
        ret = crate::ecs::dispatch_window_message(&world, entity, &m);
    });

    assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
    // 戻り値は不変(`None`=`DefWindowProcW` へ委譲)。
    assert!(
        ret.is_none(),
        "観測を足したことで WM_DISPLAYCHANGE の戻り値が変わっている(既定手続きへの委譲が失われる)"
    );
    // 既存の業務ロジックも不変。
    assert!(
        world
            .borrow()
            .world()
            .resource::<App>()
            .display_configuration_changed(),
        "表示構成変更フラグが立っていない(既存の挙動を壊している)"
    );

    let msgs = lines_of_kind(&captured, KIND_MSG);
    assert_eq!(msgs.len(), 1, "受理 1 回につき 1 行: {captured}");
    assert!(
        msgs[0].contains(&format!("msg={MSG_DISPLAYCHANGE} "))
            && msgs[0].contains(&hwnd_field(HWND(0x4321 as *mut _))),
        "メッセージ名と対象ハンドルが載る: {}",
        msgs[0]
    );
    clean_slate();
}

// ---------------------------------------------------------------------------
// 窓の拡大率の起点(`kind=windpi`)——メッセージ受理の経路(task 8.3)
// ---------------------------------------------------------------------------

#[test]
fn dpichanged_records_the_window_origin_when_the_component_is_rewritten() {
    let (captured, entity) = capture_dpichanged_with_entity(SIGNOFF_DIRECTIVES, true);
    assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");

    let records = lines_of_kind(&captured, KIND_WINDPI);
    assert_eq!(
        records.len(),
        1,
        "component の書き換え 1 回につき 1 行: {captured}"
    );
    let line = records[0];
    assert!(
        line.contains(&format!("entity={entity:?}")),
        "書き換えた窓の entity が載る: {line}"
    );
    assert!(
        line.contains("old_dpi=96 new_dpi=192"),
        "新旧の表示 DPI が同一行から復元できない: {line}"
    );
}

/// **書き換えが起きなかった経路からは発行しない**(task 8.3 の完了条件)。
///
/// `DPI` component を持たない窓では `WM_DPICHANGED` の書き換え腕へ入らない。
/// 同じ捕捉窓に受理の記録(`kind=msg`)が出ていることが、捕捉が生きている証拠である。
#[test]
fn dpichanged_records_no_window_origin_without_a_dpi_component() {
    let (captured, _entity) = capture_dpichanged_with_entity(SIGNOFF_DIRECTIVES, false);
    assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
    assert_eq!(
        lines_of_kind(&captured, KIND_MSG).len(),
        1,
        "対照の受理行が出ていない=檻の前提が崩れている: {captured}"
    );
    assert!(
        lines_of_kind(&captured, KIND_WINDPI).is_empty(),
        "component を書き換えていないのに起点が出ている: {captured}"
    );
}

#[test]
fn the_window_origin_is_silent_under_the_default_directives() {
    let (captured, _entity) = capture_dpichanged_with_entity(DEFAULT_DIRECTIVES, true);
    assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
    assert!(
        lines_of_kind(&captured, KIND_WINDPI).is_empty(),
        "観測 target を点けない運転では 1 行も出ない: {captured}"
    );
}

// ---------------------------------------------------------------------------
// 採用時の同期書込(`stage=sync`)——採否の判定は変えない
// ---------------------------------------------------------------------------

#[test]
fn applied_suggested_position_is_recorded_as_a_synchronous_write() {
    // 政策未宣言=従来どおり OS 提案位置を採る窓(`applied=true`)。
    let captured = capture_dpichanged(
        "info,wintf::ecs::window_proc=debug,wintf::transition=debug",
        None,
    );

    // 採否判定の**既存ログ**と並存すること(design Testing Strategy > Integration Tests 7)。
    assert!(
        captured.contains("[WM_DPICHANGED] suggested position write decision")
            && captured.contains("applied=true"),
        "採否判定の既存ログが失われている(判定を変えていないことの対照): {captured}"
    );

    let writes = lines_of_kind(&captured, KIND_WRITE);
    assert_eq!(
        writes.len(),
        1,
        "採用した遷移では同期書込が 1 行記録される: {captured}"
    );
    let write = writes[0];
    assert!(
        write.contains(" stage=sync "),
        "一括書込(stage=flush)と区別できる段で記録する: {write}"
    );
    assert!(write.contains("seq=0"), "同期書込の通し番号は 0: {write}");
    assert!(
        write.contains("x=3210 y=140 cx=0 cy=0"),
        "採用した提案原点がそのまま載る(寸は SWP_NOSIZE ゆえ 0): {write}"
    );
    assert!(
        write.contains(&format!("origin={ORIGIN_DPI_SUGGESTED} scope=- win_kind=-")),
        "経路語は wintf 自身が名乗る(判定側は `path_a_writes` をこの語で数える)。\
         キャラ番号と窓種別は areka の語彙ゆえ番兵: {write}"
    );
    assert_ne!(
        ORIGIN_DPI_SUGGESTED, MISSING,
        "経路語が番兵と同じでは「タグの付け忘れ」と区別が付かない"
    );
    assert!(
        write.contains("ok=false"),
        "null ハンドルへの書込は失敗する: {write}"
    );
    assert!(
        write.contains("ax=- ay=- aw=- ah=-"),
        "読み戻せない窓の書込後矩形は 4 フィールドとも番兵: {write}"
    );
    assert!(write.contains("call_us="), "1 回の所要が載る: {write}");
}

/// 経路語は 3 つとも互いに異なり、番兵とも異なる——判定側が `origin` で経路を数える
/// (`path_a_writes` は `origin=dpi-suggested`)ための前提。
#[test]
fn the_synchronous_write_origin_is_distinct_from_the_enqueue_site_origins() {
    use crate::ecs::window::transition_diag::{ORIGIN_WINDOW_POS, ORIGIN_ZORDER_PAIR};

    let words = [ORIGIN_DPI_SUGGESTED, ORIGIN_WINDOW_POS, ORIGIN_ZORDER_PAIR];
    let mut sorted = words.to_vec();
    sorted.sort_unstable();
    sorted.dedup();
    assert_eq!(sorted.len(), words.len(), "経路語が互いに異なっていない");
    for word in words {
        assert_ne!(word, MISSING, "経路語が番兵と同じ: {word}");
        assert!(
            !word.contains(' '),
            "経路語に空白があると `名前=値` の辞書化が壊れる: {word}"
        );
    }
}

#[test]
fn a_declined_suggested_position_records_the_receipt_but_no_write() {
    let captured = capture_dpichanged(
        "info,wintf::ecs::window_proc=debug,wintf::transition=debug",
        Some(DpiSuggestedRectPolicy::ExternalAuthority),
    );

    // 採否判定の既存ログは「書かない」のまま(判定を変えていない・Requirement 10.2)。
    assert!(
        captured.contains("applied=false"),
        "位置権威が外部にある窓の採否判定が変わっている(`dpi-window-vanish` の規約): {captured}"
    );
    assert_eq!(
        lines_of_kind(&captured, KIND_MSG).len(),
        1,
        "書かない窓でも受理そのものは記録される: {captured}"
    );
    assert!(
        lines_of_kind(&captured, KIND_WRITE).is_empty(),
        "書かなかったのに書込レコードが出ている: {captured}"
    );
}

// ---------------------------------------------------------------------------
// 既定 OFF
// ---------------------------------------------------------------------------

#[test]
fn message_handlers_emit_nothing_under_the_default_directives() {
    for captured in [
        capture_dpichanged(DEFAULT_DIRECTIVES, None),
        capture_windowposchanged(DEFAULT_DIRECTIVES),
    ] {
        assert!(
            captured.contains(CONTROL_LINE),
            "対照が拾えていない=捕捉が死んでいる: {captured}"
        );
        assert!(
            transition_lines(&captured).is_empty(),
            "観測 target を点けない運転では観測行は 1 行も出ない: {captured}"
        );
    }
}

// ---------------------------------------------------------------------------
// 実窓での再入(`SetWindowPos` の内側での同期受理)
// ---------------------------------------------------------------------------

thread_local! {
    /// テスト窓の wndproc が配送先とする `(World, Entity)`。未設定なら配送しない
    /// (`CreateWindowExW`/`DestroyWindow` が送るメッセージを拾わないため)。
    static DISPATCH_TARGET: RefCell<Option<(Rc<RefCell<EcsWorld>>, Entity)>> =
        const { RefCell::new(None) };
}

/// 本番と同じ配送表(`dispatch_window_message`)へ橋渡しするテスト窓の wndproc。
///
/// `runtime::wndproc_bridge::make_wndproc` と同じ規律(配送表は ECS 層が単一の真実の源・
/// `None` なら既定手続きへ委譲)を最小限で再現する。
unsafe extern "system" fn test_wndproc(
    hwnd: HWND,
    msg: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    let target = DISPATCH_TARGET.with(|cell| cell.borrow().clone());
    if let Some((world, entity)) = target {
        let message = WindowMessage {
            hwnd,
            msg,
            wparam,
            lparam,
        };
        if let Some(result) = crate::ecs::dispatch_window_message(&world, entity, &message) {
            return result;
        }
    }
    // SAFETY: Win32 境界。既定のウィンドウ手続きへそのまま委譲する。
    unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}

const TEST_CLASS: PCWSTR = w!("wintf-transition-msg-test");

/// テスト窓クラスを 1 度だけ登録する。
fn register_test_class() {
    use std::sync::Once;
    use windows::Win32::System::LibraryLoader::GetModuleHandleW;
    static ONCE: Once = Once::new();
    ONCE.call_once(|| {
        // SAFETY: Win32 境界。自プロセスに固有名のクラスを 1 度だけ登録する。
        unsafe {
            let hinstance: HINSTANCE = GetModuleHandleW(None).expect("GetModuleHandleW").into();
            let class = WNDCLASSEXW {
                cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
                lpfnWndProc: Some(test_wndproc),
                hInstance: hinstance,
                lpszClassName: TEST_CLASS,
                ..Default::default()
            };
            assert_ne!(
                RegisterClassExW(&class),
                0,
                "テスト窓クラスの登録に失敗した"
            );
        }
    });
}

/// 0x0 の不可視の道具窓を作る(`SetWindowPos` は不可視窓にも
/// `WM_WINDOWPOSCHANGED` を**同期送達**するので表示は要らない)。
fn create_test_window() -> (HWND, HINSTANCE) {
    use windows::Win32::System::LibraryLoader::GetModuleHandleW;
    register_test_class();
    // SAFETY: Win32 境界。自プロセス所有の 0x0 窓を生成する。
    unsafe {
        let hinstance: HINSTANCE = GetModuleHandleW(None).expect("GetModuleHandleW").into();
        let hwnd = CreateWindowExW(
            WINDOW_EX_STYLE(WS_EX_TOOLWINDOW.0),
            TEST_CLASS,
            w!("transition-msg"),
            WS_POPUP,
            10,
            10,
            0,
            0,
            None,
            None,
            Some(hinstance),
            None,
        )
        .expect("CreateWindowExW should create a test window");
        (hwnd, hinstance)
    }
}

/// **一括書込の内側での同期受理**が `in_swp=1` と非番兵の `since_flush_us` で残ること。
///
/// これが本仕様が追っている機序そのもの(再観測 §5=各窓の最初の `SetWindowPos` の
/// 内側で当該窓の `WM_DPICHANGED` が同期処理される 24/24)の**測り口**である。
/// 実窓が要るがメッセージポンプは要らない——`SetWindowPos` は同一スレッドの窓へ
/// 同期 send するため、`flush` の呼出がそのまま handler の呼出になる。
///
/// あわせて **flush 区間の入れ子**(`command.rs::flush` の doc)を実配線で確かめる:
/// handler の手順③が `flush_window_pos_commands()` を無条件に呼ぶため区間は入れ子に
/// なるが、内側の解除が起点を消していれば `since_flush_us` が番兵に落ちて本檻が赤になる。
#[test]
fn windowposchanged_received_inside_a_window_write_carries_in_swp_and_flush_elapsed() {
    clean_slate();
    let (hwnd, instance) = create_test_window();

    let world = Rc::new(RefCell::new(EcsWorld::new()));
    let entity = world
        .borrow_mut()
        .world_mut()
        .spawn((
            WindowHandle { hwnd, instance },
            crate::ecs::window::WindowPos::default(),
            DPI::from_dpi(96, 96),
        ))
        .id();
    DISPATCH_TARGET.with(|cell| *cell.borrow_mut() = Some((world.clone(), entity)));

    let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
        emit_control();
        SetWindowPosCommand::enqueue(SetWindowPosCommand::new(
            hwnd,
            30,
            40,
            0,
            0,
            SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
            None,
        ));
        crate::ecs::window::flush_window_pos_commands();
    });

    // 配送を止めてから窓を壊す(`DestroyWindow` のメッセージを拾わない)。
    DISPATCH_TARGET.with(|cell| *cell.borrow_mut() = None);
    // SAFETY: Win32 境界。自プロセスが生成した窓を破棄する。
    unsafe {
        let _ = DestroyWindow(hwnd);
    }
    clean_slate();

    assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");

    // 探針の自己検査: 一括書込が実際に走っている(走っていなければ以下は空虚)。
    let lines = transition_lines(&captured);
    let begin_at = lines
        .iter()
        .position(|l| l.contains("kind=flush ") && l.contains("stage=begin"))
        .unwrap_or_else(|| panic!("一括書込の開始行が無い: {captured}"));
    let write_at = lines
        .iter()
        .position(|l| l.contains("kind=write "))
        .unwrap_or_else(|| panic!("書込レコードが無い: {captured}"));

    let msg_at = lines
        .iter()
        .position(|l| {
            l.contains("kind=msg ") && l.contains(&format!("msg={MSG_WINDOWPOSCHANGED} "))
        })
        .unwrap_or_else(|| {
            panic!("`SetWindowPos` が同期送達した受理が記録されていない: {captured}")
        });
    let line = lines[msg_at];

    assert!(
        line.contains(&hwnd_field(hwnd)),
        "受理行が対象の実窓を指していない: {line}"
    );
    assert!(
        line.contains("in_swp=true"),
        "`SetWindowPos` の内側での受理なのに in_swp が偽(同期送達の証跡が消えている): {line}"
    );
    assert!(
        !line.contains("since_flush_us=-"),
        "一括書込の内側での受理なのに経過が番兵(入れ子の解除が外側の起点まで消している): {line}"
    );

    // 時系列: 開始 → 受理(`SetWindowPos` の内側)→ 書込レコード。
    assert!(
        begin_at < msg_at && msg_at < write_at,
        "受理が一括書込の区間の内側に並んでいない(begin={begin_at} msg={msg_at} write={write_at}): {captured}"
    );
}

// ---------------------------------------------------------------------------
// 前置ガード(構造で固定する——出力では示せない)
// ---------------------------------------------------------------------------

/// 説明文(`//` で始まる行)を落とし、コードだけの本文を返す。
fn code_only(source: &str) -> String {
    source
        .lines()
        .filter(|line| !line.trim_start().starts_with("//"))
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn message_handlers_keep_the_front_guard_so_the_default_run_pays_no_observation_cost() {
    // 「既定の directive で 1 行も出ない」は前置ガードを外しても成立してしまう
    // ——`emit_line` の水準(debug)を subscriber が濾すからである。よって
    // **費用を払わないこと**(行の組立・`Instant` の読み取り・`GetWindowRect` の
    // 読み戻し)は出力では示せず、構造で固定するほかない。
    //
    // ガードそのものの真偽が directive に追随することは
    // `transition_diag_tests::is_enabled_follows_the_directive` が実濾過で示している。
    let code = code_only(include_str!("window_pos.rs"));

    // 記録を組む点は 4 つ(`WM_DPICHANGED` の受理・`WM_WINDOWPOSCHANGED` の受理・
    // 採用時の同期書込・`WM_DPICHANGED` での窓 DPI 書き換え=`kind=windpi`・task 8.3)。
    // 刻印を読む回数とガードの回数が一致していなければ、どれかがガードの外に出ている。
    assert_eq!(
        code.matches("transition_diag::is_enabled()").count(),
        4,
        "前置ガードの数が記録点の数と食い違う——ガードされていない記録点がある"
    );
    assert_eq!(
        code.matches("transition_diag::stamp()").count(),
        4,
        "刻印を読む回数が記録点の数と食い違う"
    );
    assert_eq!(
        code.matches("transition_diag::since_flush_us()").count(),
        2,
        "受理レコードは 2 点(`WM_DPICHANGED`・`WM_WINDOWPOSCHANGED`)だけが経過を読む"
    );
    // 同期書込の計時と読み戻しはガード変数の内側だけ。
    assert!(
        code.contains("let observe = transition_diag::is_enabled();"),
        "同期書込の前置ガードが失われている——既定運転でも計時と読み戻しの費用を払う"
    );
    assert!(
        code.contains("observe.then(Instant::now)"),
        "同期書込の計時がガードの外で行われている"
    );
    assert_eq!(
        code.matches("Instant::now").count(),
        1,
        "本文に `Instant::now` が 2 度以上ある(ガードされていない計時が混ざり得る)"
    );
    assert_eq!(
        code.matches("read_back_window_rect(").count(),
        1,
        "書込後矩形の読み戻しの呼出が 1 箇所でない(ガードの外から呼ばれ得る)"
    );

    // 走査が中身を見ている対照——落とし過ぎていないこと。
    assert!(
        code.contains("pub(super) fn WM_WINDOWPOSCHANGED("),
        "説明文を落とす処理が本文まで落としている"
    );

    // 表示構成変更の受理も同じ形(ガードの内側で 1 回だけ組む)。
    let lifecycle = code_only(include_str!("lifecycle.rs"));
    assert_eq!(
        lifecycle.matches("transition_diag::is_enabled()").count(),
        1,
        "`WM_DISPLAYCHANGE` の記録点の前置ガードが 1 つでない"
    );
    assert_eq!(
        lifecycle.matches("transition_diag::stamp()").count(),
        1,
        "`WM_DISPLAYCHANGE` が刻印を読む回数が 1 でない"
    );
    assert!(
        lifecycle.contains("pub(super) fn WM_DISPLAYCHANGE("),
        "説明文を落とす処理が本文まで落としている"
    );
}