@team-agent/installer 0.3.23 → 0.3.24

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.
@@ -298,6 +298,11 @@ pub fn project_top_level_view(state: &Value, team_key: &str) -> Value {
298
298
  team_entry_from_state(state, team_key).and_then(Value::as_object).cloned().unwrap_or_default();
299
299
  let mut p = entry_obj.clone(); // projection = deepcopy(entry)
300
300
  // setdefault session_name/team_dir ← entry.get(...)(缺则插,值可为 null)。
301
+ // **0.3.24 excision** (U1-A real-machine RED v2): the ff38ab9 root-state
302
+ // `or_else` fallback was reverted along with the wave-2 U1-A drift fallback,
303
+ // since this projection change had no other consumer. v0.3.25 will re-introduce
304
+ // this together with the writer-shape + rediscover-writer fix — see
305
+ // `.team/artifacts/u1-a-realmachine-v2-fix-or-excise.md`.
301
306
  if !p.contains_key("session_name") {
302
307
  p.insert("session_name".to_string(), entry_obj.get("session_name").cloned().unwrap_or(Value::Null));
303
308
  }
@@ -526,6 +526,52 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
526
526
  );
527
527
  }
528
528
 
529
+ #[test]
530
+ fn inject_waits_for_token_visibility_before_enter() {
531
+ let text = "hello [team-agent-token:e31]".to_string();
532
+ let marker_visible = format!("{text}\n");
533
+ let (be, rec) = backend_with(
534
+ MockResp::Out(ok("")),
535
+ vec![
536
+ MockResp::Out(ok("")), // set-buffer
537
+ MockResp::Out(ok("")), // paste-buffer
538
+ MockResp::Out(ok("")), // delete-buffer
539
+ MockResp::Out(ok("")), // pasted-content prompt poll 1
540
+ MockResp::Out(ok("")), // pasted-content prompt poll 2
541
+ MockResp::Out(ok("")), // pasted-content prompt poll 3
542
+ MockResp::Out(ok("")), // pasted-content prompt poll 4
543
+ MockResp::Out(ok("")), // pasted-content prompt poll 5
544
+ MockResp::Out(ok("")), // token gate: not visible yet
545
+ MockResp::Out(ok("")), // token gate: still not visible
546
+ MockResp::Out(ok(&marker_visible)), // token gate: visible, Enter may fire
547
+ MockResp::Out(ok("")), // send-keys Enter
548
+ ],
549
+ );
550
+
551
+ let report = be
552
+ .inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text), Key::Enter, true)
553
+ .expect("inject");
554
+ let calls = rec.lock().unwrap().clone();
555
+ let submit_index = calls
556
+ .iter()
557
+ .position(|argv| argv.get(1).map(String::as_str) == Some("send-keys"))
558
+ .expect("inject must eventually send Enter");
559
+ let captures_before_submit = calls[..submit_index]
560
+ .iter()
561
+ .filter(|argv| argv.get(1).map(String::as_str) == Some("capture-pane"))
562
+ .count();
563
+
564
+ assert!(
565
+ captures_before_submit >= super::PASTED_CONTENT_APPEAR_POLLS as usize + 3,
566
+ "Enter must wait until the pasted token is visible; calls={calls:?}"
567
+ );
568
+ assert_eq!(report.inject_verification, InjectVerification::CaptureContainsToken);
569
+ assert_eq!(
570
+ report.submit_verification,
571
+ SubmitVerification::EnterSentWithoutPlaceholderCheck
572
+ );
573
+ }
574
+
529
575
  #[test]
530
576
  fn send_keys_cancel_mode_queries_mode_and_dispatches_cancel_argv() {
531
577
  let (be, rec) = backend_with(
@@ -845,16 +845,28 @@ fn token_visible_in_capture(
845
845
  }
846
846
  }
847
847
 
848
- /// U1 #7: capture the pane just before submit and report whether the just-pasted
849
- /// token marker is actually visible. `Ok(None)` for non-token payloads (nothing to
850
- /// check). `Ok(Some(false))` means the paste silently dropped a false-positive that
851
- /// the static `inject_verification_for_payload` would have reported as success.
848
+ /// U1 #7 / E31: wait briefly for the just-pasted token marker before submitting.
849
+ /// `Ok(None)` for non-token payloads (nothing to check). `Ok(Some(false))` means the
850
+ /// paste did not become visible before the Python-parity fallback delay.
852
851
  fn pre_submit_token_visible(
853
852
  backend: &TmuxBackend,
854
853
  target: &Target,
855
854
  payload: &InjectPayload,
856
855
  ) -> Result<Option<bool>, TransportError> {
857
- token_visible_in_capture(backend, target, payload)
856
+ if payload_token_marker(payload).is_none() {
857
+ return Ok(None);
858
+ }
859
+ for attempt in 0..PASTED_CONTENT_APPEAR_POLLS {
860
+ if let Some(true) = token_visible_in_capture(backend, target, payload)? {
861
+ return Ok(Some(true));
862
+ }
863
+ if attempt + 1 < PASTED_CONTENT_APPEAR_POLLS {
864
+ std::thread::sleep(Duration::from_millis(25));
865
+ }
866
+ }
867
+ // Python waits 250ms between paste-buffer and Enter to let bracketed paste settle.
868
+ std::thread::sleep(Duration::from_millis(250));
869
+ Ok(Some(false))
858
870
  }
859
871
 
860
872
  const TOKEN_POST_SUBMIT_READBACK_POLLS: u32 = 5;
@@ -37,6 +37,18 @@ struct OfflineState {
37
37
  inject_targets: Vec<Target>,
38
38
  inject_payloads: Vec<String>,
39
39
  tmux_endpoint: Option<String>,
40
+ /// U1-B contract: when set, `list_targets()` returns `TransportError::MuxUnavailable`
41
+ /// — used to model tmux server jitter (subprocess-fork-failed level). The whole
42
+ /// resolve must DEFER, not silently coerce to an empty vec.
43
+ list_targets_error: Option<String>,
44
+ /// U1-C / general: pre-staged `capture()` payload, keyed by target stringification.
45
+ /// `Target::Pane(p)` keys as `p.as_str()`; `Target::SessionWindow{session,window}`
46
+ /// keys as `format!("{session}:{window}")`. A miss returns empty text (current
47
+ /// default behaviour).
48
+ capture_text: BTreeMap<String, String>,
49
+ /// U1-C Tail-peek contract: every `capture()` call records its `CaptureRange`,
50
+ /// in order, so a test can prove the delivery peek site narrowed Full → Tail(80).
51
+ capture_ranges: Vec<CaptureRange>,
40
52
  }
41
53
 
42
54
  impl Default for OfflineState {
@@ -57,6 +69,9 @@ impl Default for OfflineState {
57
69
  inject_targets: Vec::new(),
58
70
  inject_payloads: Vec::new(),
59
71
  tmux_endpoint: None,
72
+ list_targets_error: None,
73
+ capture_text: BTreeMap::new(),
74
+ capture_ranges: Vec::new(),
60
75
  }
61
76
  }
62
77
  }
@@ -127,6 +142,40 @@ impl OfflineTransport {
127
142
  self
128
143
  }
129
144
 
145
+ /// U1-B: stage a `list_targets()` error to model tmux server jitter. While set,
146
+ /// every call returns `Err(TransportError::MuxUnavailable{detail})`. Pass an
147
+ /// empty string to clear via re-builder if needed.
148
+ pub fn with_list_targets_error(self, detail: impl Into<String>) -> Self {
149
+ self.with_state(|state| state.list_targets_error = Some(detail.into()));
150
+ self
151
+ }
152
+
153
+ /// Pre-stage `capture()` output for a `Target::Pane(pane_id)` key.
154
+ pub fn with_capture_for_pane(
155
+ self,
156
+ pane_id: impl Into<String>,
157
+ text: impl Into<String>,
158
+ ) -> Self {
159
+ self.with_state(|state| {
160
+ state.capture_text.insert(pane_id.into(), text.into());
161
+ });
162
+ self
163
+ }
164
+
165
+ /// Pre-stage `capture()` output for a `Target::SessionWindow{session,window}` key.
166
+ pub fn with_capture_for_session_window(
167
+ self,
168
+ session: impl Into<String>,
169
+ window: impl Into<String>,
170
+ text: impl Into<String>,
171
+ ) -> Self {
172
+ let key = format!("{}:{}", session.into(), window.into());
173
+ self.with_state(|state| {
174
+ state.capture_text.insert(key, text.into());
175
+ });
176
+ self
177
+ }
178
+
130
179
  pub fn calls(&self) -> Vec<&'static str> {
131
180
  self.with_state(|state| state.calls.clone())
132
181
  }
@@ -163,6 +212,12 @@ impl OfflineTransport {
163
212
  self.with_state(|state| state.inject_payloads.clone())
164
213
  }
165
214
 
215
+ /// All `capture()` ranges observed, in call order. Used by U1-C Tail-peek
216
+ /// contracts to prove the delivery peek site requested Tail rather than Full.
217
+ pub fn capture_ranges(&self) -> Vec<CaptureRange> {
218
+ self.with_state(|state| state.capture_ranges.clone())
219
+ }
220
+
166
221
  fn record(&self, call: &'static str) {
167
222
  self.with_state(|state| state.calls.push(call));
168
223
  }
@@ -294,11 +349,21 @@ impl Transport for OfflineTransport {
294
349
 
295
350
  fn capture(
296
351
  &self,
297
- _target: &Target,
352
+ target: &Target,
298
353
  range: CaptureRange,
299
354
  ) -> Result<CapturedText, TransportError> {
300
- self.record("capture");
301
- Ok(CapturedText { text: String::new(), range })
355
+ let key = match target {
356
+ Target::Pane(p) => p.as_str().to_string(),
357
+ Target::SessionWindow { session, window } => {
358
+ format!("{}:{}", session.as_str(), window.as_str())
359
+ }
360
+ };
361
+ let text = self.with_state(|state| {
362
+ state.calls.push("capture");
363
+ state.capture_ranges.push(range);
364
+ state.capture_text.get(&key).cloned().unwrap_or_default()
365
+ });
366
+ Ok(CapturedText { text, range })
302
367
  }
303
368
 
304
369
  fn query(
@@ -329,10 +394,18 @@ impl Transport for OfflineTransport {
329
394
  }
330
395
 
331
396
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
332
- Ok(self.with_state(|state| {
397
+ // U1-B: when `with_list_targets_error` is set, return a real Err so the
398
+ // caller sees server jitter rather than a coerced empty vec.
399
+ if let Some(detail) = self.with_state(|state| {
333
400
  state.calls.push("list_targets");
334
- state.targets.clone()
335
- }))
401
+ state.list_targets_error.clone()
402
+ }) {
403
+ return Err(TransportError::MuxUnavailable {
404
+ backend: BackendKind::Tmux,
405
+ detail,
406
+ });
407
+ }
408
+ Ok(self.with_state(|state| state.targets.clone()))
336
409
  }
337
410
 
338
411
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.23",
3
+ "version": "0.3.24",
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.3.23",
24
- "@team-agent/cli-darwin-x64": "0.3.23",
25
- "@team-agent/cli-linux-x64": "0.3.23"
23
+ "@team-agent/cli-darwin-arm64": "0.3.24",
24
+ "@team-agent/cli-darwin-x64": "0.3.24",
25
+ "@team-agent/cli-linux-x64": "0.3.24"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",