@team-agent/installer 0.5.45 → 0.5.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,6 +39,36 @@ struct MockCommandRunner {
39
39
  default: MockResp,
40
40
  }
41
41
 
42
+ /// Models the real build-before-destroy overlap: the spawn command creates `%new`, while a
43
+ /// name-based lookup still resolves the same-named old window `%old`.
44
+ struct SameNameSpawnRunner {
45
+ recorded: RecordedArgv,
46
+ }
47
+
48
+ impl CommandRunner for SameNameSpawnRunner {
49
+ fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
50
+ self.recorded.lock().unwrap().push(argv.to_vec());
51
+ let output = match argv.get(1).map(String::as_str) {
52
+ Some("new-window") => ok("%new\n"),
53
+ Some("display-message") => ok("%old\n"),
54
+ Some("list-panes") => ok(
55
+ "%old\tteamsess\t0\tdeveloper\t0\t/dev/ttys001\tnode\t1\t/work/dir\t1\t0\t101\n\
56
+ %new\tteamsess\t1\tdeveloper\t0\t/dev/ttys002\tnode\t1\t/work/dir\t1\t0\t202\n",
57
+ ),
58
+ _ => fail(64, "unexpected scripted tmux command"),
59
+ };
60
+ Ok(output)
61
+ }
62
+
63
+ fn run_with_stdin(
64
+ &self,
65
+ argv: &[String],
66
+ _stdin: &str,
67
+ ) -> Result<CommandOutput, std::io::Error> {
68
+ self.run(argv)
69
+ }
70
+ }
71
+
42
72
  impl CommandRunner for MockCommandRunner {
43
73
  fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
44
74
  self.recorded.lock().unwrap().push(argv.to_vec());
@@ -366,8 +396,7 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
366
396
  let (be, rec) = backend_with(
367
397
  MockResp::Out(ok("")),
368
398
  vec![
369
- MockResp::Out(ok("")),
370
- MockResp::Out(ok("%3")),
399
+ MockResp::Out(ok("%3\n")),
371
400
  MockResp::Out(ok(pane_inventory)),
372
401
  ],
373
402
  );
@@ -383,7 +412,8 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
383
412
  &env,
384
413
  )
385
414
  .expect("spawn_first");
386
- let argv = rec.lock().unwrap()[0].clone();
415
+ let calls = rec.lock().unwrap().clone();
416
+ let argv = calls[0].clone();
387
417
  let cmd = argv.last().expect("the sh -lc command string").clone();
388
418
  assert_eq!(
389
419
  argv,
@@ -400,6 +430,12 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
400
430
  "SpawnResult.pane_id must parse from the tmux output"
401
431
  );
402
432
  assert_eq!(result.child_pid, Some(123));
433
+ assert!(
434
+ calls
435
+ .iter()
436
+ .all(|call| call.get(1).is_none_or(|arg| arg != "display-message")),
437
+ "spawn_first identity must come from new-session stdout, never a display-message lookup; calls={calls:?}"
438
+ );
403
439
  }
404
440
 
405
441
  #[test]
@@ -408,8 +444,7 @@ fn spawn_into_frames_via_new_window_builder() {
408
444
  let (be, rec) = backend_with(
409
445
  MockResp::Out(ok("")),
410
446
  vec![
411
- MockResp::Out(ok("")),
412
- MockResp::Out(ok("%4")),
447
+ MockResp::Out(ok("%4\n")),
413
448
  MockResp::Out(ok(pane_inventory)),
414
449
  ],
415
450
  );
@@ -424,7 +459,8 @@ fn spawn_into_frames_via_new_window_builder() {
424
459
  &BTreeMap::new(),
425
460
  )
426
461
  .expect("spawn_into");
427
- let argv = rec.lock().unwrap()[0].clone();
462
+ let calls = rec.lock().unwrap().clone();
463
+ let argv = calls[0].clone();
428
464
  let cmd = argv.last().expect("the sh -lc command string").clone();
429
465
  assert_eq!(
430
466
  argv,
@@ -432,16 +468,58 @@ fn spawn_into_frames_via_new_window_builder() {
432
468
  "spawn_into must frame via tmux_spawn_argv first=false (new-window -t <s> -n <w> sh -lc <cmd>)"
433
469
  );
434
470
  assert_eq!(result.pane_id.as_str(), "%4");
471
+ assert!(
472
+ calls
473
+ .iter()
474
+ .all(|call| call.get(1).is_none_or(|arg| arg != "display-message")),
475
+ "spawn_into identity must come from new-window stdout, never a display-message lookup; calls={calls:?}"
476
+ );
477
+ }
478
+
479
+ #[test]
480
+ fn spawn_into_same_named_replacement_returns_identity_created_by_spawn_command() {
481
+ let recorded = Arc::new(Mutex::new(Vec::new()));
482
+ let be = TmuxBackend::with_runner(Box::new(SameNameSpawnRunner {
483
+ recorded: Arc::clone(&recorded),
484
+ }));
485
+
486
+ let result = be
487
+ .spawn_into(
488
+ &SessionName::new("teamsess"),
489
+ &WindowName::new("developer"),
490
+ &svec(&["provider-bin"]),
491
+ Path::new("/work/dir"),
492
+ &BTreeMap::new(),
493
+ )
494
+ .expect("same-name replacement spawn");
495
+ let calls = recorded.lock().unwrap().clone();
496
+ let name_based_identity_lookup = calls.iter().any(|call| {
497
+ call.get(1).is_some_and(|arg| arg == "display-message")
498
+ && call
499
+ .iter()
500
+ .position(|arg| arg == "-t")
501
+ .and_then(|index| call.get(index + 1))
502
+ .is_some_and(|target| target == "teamsess:developer")
503
+ });
504
+
505
+ assert_eq!(
506
+ result.pane_id.as_str(),
507
+ "%new",
508
+ "spawn_into must return the pane identity atomically emitted by this new-window command, not the same-named old window; calls={calls:?}"
509
+ );
510
+ assert!(
511
+ !name_based_identity_lookup,
512
+ "spawn identity must not be re-derived through ambiguous session:window display-message; calls={calls:?}"
513
+ );
435
514
  }
436
515
 
437
516
  #[test]
438
- fn spawn_with_command_refuses_display_message_pane_owned_by_other_window() {
517
+ fn spawn_with_command_refuses_spawn_pane_owned_by_other_window() {
439
518
  let pane_inventory = "%5\tteamsess\t1\tw2\t0\t/dev/ttys005\tnode\t1\t/work/dir\t1\t0\t125\n";
440
519
  let (be, _rec) = backend_with(
441
520
  MockResp::Out(ok("")),
442
521
  vec![
443
- MockResp::Out(ok("")),
444
- MockResp::Out(ok("%5")),
522
+ MockResp::Out(ok("%5\n")),
445
523
  MockResp::Out(ok(pane_inventory)),
446
524
  ],
447
525
  );
@@ -453,7 +531,7 @@ fn spawn_with_command_refuses_display_message_pane_owned_by_other_window() {
453
531
  Path::new("/work/dir"),
454
532
  &BTreeMap::new(),
455
533
  )
456
- .expect_err("display-message fallback to w2 pane must fail closed");
534
+ .expect_err("spawn stdout pane owned by w2 must fail closed");
457
535
  let msg = err.to_string();
458
536
  assert!(
459
537
  msg.contains("requested=teamsess:w1")
@@ -463,6 +541,34 @@ fn spawn_with_command_refuses_display_message_pane_owned_by_other_window() {
463
541
  );
464
542
  }
465
543
 
544
+ #[test]
545
+ fn spawn_with_command_empty_stdout_fails_closed_without_identity_fallback() {
546
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![MockResp::Out(ok(""))]);
547
+ let err = be
548
+ .spawn_into(
549
+ &SessionName::new("teamsess"),
550
+ &WindowName::new("developer"),
551
+ &svec(&["provider-bin"]),
552
+ Path::new("/work/dir"),
553
+ &BTreeMap::new(),
554
+ )
555
+ .expect_err("empty new-window stdout must fail closed");
556
+ let msg = err.to_string().to_ascii_lowercase();
557
+ let calls = rec.lock().unwrap().clone();
558
+ let used_identity_fallback = calls
559
+ .iter()
560
+ .any(|call| call.get(1).is_some_and(|arg| arg == "display-message"));
561
+
562
+ assert!(
563
+ msg.contains("spawn") && (msg.contains("empty") || msg.contains("no pane id")),
564
+ "empty spawn stdout error must name the missing spawn identity; error={err}; calls={calls:?}"
565
+ );
566
+ assert!(
567
+ !used_identity_fallback,
568
+ "empty spawn stdout must not fall back to ambiguous display-message identity lookup; calls={calls:?}"
569
+ );
570
+ }
571
+
466
572
  #[test]
467
573
  fn spawn_split_selects_even_horizontal_not_tiled() {
468
574
  let (be, rec) = backend_with(
@@ -765,25 +765,14 @@ impl TmuxBackend {
765
765
  first: bool,
766
766
  ) -> Result<SpawnResult, TransportError> {
767
767
  let spawn_argv = tmux_spawn_argv(session, window, command, first);
768
- self.run_spawn(&spawn_argv)?;
769
- let pane_argv = vec![
770
- "tmux".to_string(),
771
- "display-message".to_string(),
772
- "-p".to_string(),
773
- "-t".to_string(),
774
- format!("{}:{}", session.as_str(), window.as_str()),
775
- "#{pane_id}".to_string(),
776
- ];
777
- let output = self.run_spawn(&pane_argv)?;
768
+ let output = self.run_spawn(&spawn_argv)?;
778
769
  let pane = output.stdout.trim();
779
- // T3-5 (harvest §1): never fabricate a `%0` pane id on an empty reply — a fake
780
- // pane id mis-addresses every later inject/capture/kill. Surface the miss.
781
770
  if pane.is_empty() {
782
771
  return Err(TransportError::Subprocess {
783
- argv: pane_argv,
772
+ argv: spawn_argv,
784
773
  code: output.code,
785
774
  stderr: format!(
786
- "tmux display-message returned no pane id for {}:{}",
775
+ "tmux spawn returned no pane id for {}:{}",
787
776
  session.as_str(),
788
777
  window.as_str()
789
778
  ),
@@ -822,10 +811,10 @@ impl TmuxBackend {
822
811
  })
823
812
  .unwrap_or_else(|| "<missing-from-list-targets>".to_string());
824
813
  Err(TransportError::Subprocess {
825
- argv: pane_argv,
814
+ argv: spawn_argv,
826
815
  code: output.code,
827
816
  stderr: format!(
828
- "tmux spawn pane ownership mismatch: requested={}:{} observed_pane={} observed={}",
817
+ "tmux spawn pane identity mismatch: requested={}:{} observed_pane={} observed={}",
829
818
  session.as_str(),
830
819
  window.as_str(),
831
820
  pane_id.as_str(),
@@ -238,6 +238,9 @@ fn spawn_first_returns_stable_addressable_target_then_reachable() {
238
238
  "tmux",
239
239
  "new-session",
240
240
  "-d",
241
+ "-P",
242
+ "-F",
243
+ "#{pane_id}",
241
244
  "-s",
242
245
  "team-sess",
243
246
  "-n",
@@ -287,6 +290,9 @@ fn spawn_into_then_list_targets_enumerates_it() {
287
290
  "tmux",
288
291
  "new-window",
289
292
  "-d",
293
+ "-P",
294
+ "-F",
295
+ "#{pane_id}",
290
296
  "-t",
291
297
  "team-sess",
292
298
  "-n",
@@ -918,8 +918,8 @@ pub fn tmux_query_argv(pane: &PaneId, field: PaneField) -> Vec<String> {
918
918
 
919
919
  /// spawn argv:首个 session 用 new-session,后续 worker 用 new-window。
920
920
  /// golden(terminal.py:44-45 / runtime.py:1019-1020):
921
- /// first → `new-session -d -s <s> -n <w> sh -lc <cmd>`
922
- /// into → `new-window -t <s> -n <w> sh -lc <cmd>`
921
+ /// first → `new-session -d -P -F #{pane_id} -s <s> -n <w> sh -lc <cmd>`
922
+ /// into → `new-window -d -P -F #{pane_id} -t <s> -n <w> sh -lc <cmd>`
923
923
  /// `argv` 被组装成单条 `sh -lc` 命令字符串(provider 启动行)。
924
924
  pub fn tmux_spawn_argv(
925
925
  session: &SessionName,
@@ -932,6 +932,9 @@ pub fn tmux_spawn_argv(
932
932
  "tmux".to_string(),
933
933
  "new-session".to_string(),
934
934
  "-d".to_string(),
935
+ "-P".to_string(),
936
+ "-F".to_string(),
937
+ "#{pane_id}".to_string(),
935
938
  "-s".to_string(),
936
939
  session.as_str().to_string(),
937
940
  "-n".to_string(),
@@ -952,6 +955,9 @@ pub fn tmux_spawn_argv(
952
955
  "tmux".to_string(),
953
956
  "new-window".to_string(),
954
957
  "-d".to_string(),
958
+ "-P".to_string(),
959
+ "-F".to_string(),
960
+ "#{pane_id}".to_string(),
955
961
  "-t".to_string(),
956
962
  session.as_str().to_string(),
957
963
  "-n".to_string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.45",
3
+ "version": "0.5.46",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.45",
24
- "@team-agent/cli-darwin-x64": "0.5.45",
25
- "@team-agent/cli-linux-x64": "0.5.45"
23
+ "@team-agent/cli-darwin-arm64": "0.5.46",
24
+ "@team-agent/cli-darwin-x64": "0.5.46",
25
+ "@team-agent/cli-linux-x64": "0.5.46"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",