@team-agent/installer 0.5.113 → 0.5.114
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.
- package/Cargo.lock +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +260 -16
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -153,12 +153,8 @@ enum MatchResult<'a> {
|
|
|
153
153
|
|
|
154
154
|
/// The CLI's concise JSON projection. `agent` filters the registered list but
|
|
155
155
|
/// never synthesizes an unregistered node.
|
|
156
|
-
pub(crate) fn status_brief_scoped(
|
|
157
|
-
|
|
158
|
-
state: &Value,
|
|
159
|
-
agent: Option<&str>,
|
|
160
|
-
) -> Value {
|
|
161
|
-
let nodes = project_status_nodes(state, agent);
|
|
156
|
+
pub(crate) fn status_brief_scoped(workspace: &Path, state: &Value, agent: Option<&str>) -> Value {
|
|
157
|
+
let nodes = project_status_nodes(workspace, state, agent);
|
|
162
158
|
json!({
|
|
163
159
|
"nodes": nodes
|
|
164
160
|
.into_iter()
|
|
@@ -169,12 +165,8 @@ pub(crate) fn status_brief_scoped(
|
|
|
169
165
|
|
|
170
166
|
/// Human output keeps the legacy single-node form and groups shared attach
|
|
171
167
|
/// context only when multiple nodes make that repetition useful.
|
|
172
|
-
pub(crate) fn format_status_brief(
|
|
173
|
-
|
|
174
|
-
state: &Value,
|
|
175
|
-
agent: Option<&str>,
|
|
176
|
-
) -> String {
|
|
177
|
-
let nodes = project_status_nodes(state, agent);
|
|
168
|
+
pub(crate) fn format_status_brief(workspace: &Path, state: &Value, agent: Option<&str>) -> String {
|
|
169
|
+
let nodes = project_status_nodes(workspace, state, agent);
|
|
178
170
|
if nodes.len() <= 1 {
|
|
179
171
|
return nodes
|
|
180
172
|
.into_iter()
|
|
@@ -186,14 +178,47 @@ pub(crate) fn format_status_brief(
|
|
|
186
178
|
}
|
|
187
179
|
|
|
188
180
|
fn project_status_nodes(
|
|
181
|
+
_workspace: &Path,
|
|
189
182
|
state: &Value,
|
|
190
183
|
agent: Option<&str>,
|
|
191
184
|
) -> Vec<(BriefNode, Option<AttachContext>)> {
|
|
192
185
|
let registered = registered_nodes(state, agent);
|
|
193
|
-
let
|
|
186
|
+
let legacy: Vec<_> = registered
|
|
187
|
+
.iter()
|
|
188
|
+
.filter(|_node| {
|
|
189
|
+
#[cfg(unix)]
|
|
190
|
+
if crate::contract_runtime::registry::descriptor(&_node.provider).is_some() {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
true
|
|
194
|
+
})
|
|
195
|
+
.cloned()
|
|
196
|
+
.collect();
|
|
197
|
+
let probes = probe_registered_endpoints(&legacy);
|
|
194
198
|
registered
|
|
195
199
|
.iter()
|
|
196
200
|
.map(|node| {
|
|
201
|
+
#[cfg(unix)]
|
|
202
|
+
if crate::contract_runtime::registry::descriptor(&node.provider).is_some() {
|
|
203
|
+
let mut brief = BriefNode::unknown(node);
|
|
204
|
+
if let Ok(native) = crate::contract_runtime::framework::project_seat(
|
|
205
|
+
_workspace,
|
|
206
|
+
&crate::state::projection::team_state_key(state),
|
|
207
|
+
&node.name,
|
|
208
|
+
) {
|
|
209
|
+
brief.runtime_status = native["status"].as_str().unwrap_or("unknown").into();
|
|
210
|
+
brief.model = native["model"].as_str().map(str::to_owned);
|
|
211
|
+
brief.effort = native["effort"].as_str().map(str::to_owned);
|
|
212
|
+
if brief.runtime_status == "running" {
|
|
213
|
+
brief.tmux_command = native["native_endpoint"].as_str().map(|endpoint| {
|
|
214
|
+
format!("tmux -S {} attach-session", shell_quote(endpoint))
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Process alive is T1 only: activity/health remain unknown until
|
|
219
|
+
// independently observed. Never probe a private %0 on old tmux.
|
|
220
|
+
return (brief, None);
|
|
221
|
+
}
|
|
197
222
|
project_node_with_attach(node, probes.get(node.endpoint.as_deref().unwrap_or("")))
|
|
198
223
|
})
|
|
199
224
|
.collect()
|
|
@@ -276,10 +301,7 @@ pub(crate) fn with_test_nodeprobe_resolver_delay<R>(
|
|
|
276
301
|
}
|
|
277
302
|
|
|
278
303
|
#[cfg(test)]
|
|
279
|
-
fn with_test_nodeprobe_candidate<R>(
|
|
280
|
-
paths: Vec<PathBuf>,
|
|
281
|
-
func: impl FnOnce() -> R,
|
|
282
|
-
) -> R {
|
|
304
|
+
fn with_test_nodeprobe_candidate<R>(paths: Vec<PathBuf>, func: impl FnOnce() -> R) -> R {
|
|
283
305
|
struct Guard(Option<Vec<PathBuf>>);
|
|
284
306
|
impl Drop for Guard {
|
|
285
307
|
fn drop(&mut self) {
|
|
@@ -322,8 +344,11 @@ fn write_test_capability_receipt(path: &Path) {
|
|
|
322
344
|
"capabilities": NODEPROBE_CAPABILITIES,
|
|
323
345
|
"forbidden": NODEPROBE_FORBIDDEN,
|
|
324
346
|
});
|
|
325
|
-
std::fs::write(
|
|
326
|
-
|
|
347
|
+
std::fs::write(
|
|
348
|
+
nodeprobe_receipt_path(path),
|
|
349
|
+
serde_json::to_vec(&receipt).unwrap(),
|
|
350
|
+
)
|
|
351
|
+
.expect("write test nodeprobe capability receipt");
|
|
327
352
|
}
|
|
328
353
|
|
|
329
354
|
fn format_brief_node(value: &Value) -> Option<String> {
|
|
@@ -345,9 +370,10 @@ fn format_multi_node_brief(nodes: Vec<(BriefNode, Option<AttachContext>)>) -> St
|
|
|
345
370
|
let mut groups = Vec::<(String, String)>::new();
|
|
346
371
|
for (_, context) in &nodes {
|
|
347
372
|
let Some(context) = context else { continue };
|
|
348
|
-
if !groups
|
|
349
|
-
|
|
350
|
-
|
|
373
|
+
if !groups
|
|
374
|
+
.iter()
|
|
375
|
+
.any(|(endpoint, session)| endpoint == &context.endpoint && session == &context.session)
|
|
376
|
+
{
|
|
351
377
|
groups.push((context.endpoint.clone(), context.session.clone()));
|
|
352
378
|
}
|
|
353
379
|
}
|
|
@@ -355,9 +381,7 @@ fn format_multi_node_brief(nodes: Vec<(BriefNode, Option<AttachContext>)>) -> St
|
|
|
355
381
|
let mut lines = groups
|
|
356
382
|
.iter()
|
|
357
383
|
.enumerate()
|
|
358
|
-
.map(|(index, (endpoint, session))|
|
|
359
|
-
format_attach_template(index + 1, endpoint, session)
|
|
360
|
-
})
|
|
384
|
+
.map(|(index, (endpoint, session))| format_attach_template(index + 1, endpoint, session))
|
|
361
385
|
.collect::<Vec<_>>();
|
|
362
386
|
if !groups.is_empty() {
|
|
363
387
|
lines.push(
|
|
@@ -543,12 +567,14 @@ fn probe_registered_endpoints(nodes: &[RegisteredNode]) -> BTreeMap<String, Opti
|
|
|
543
567
|
endpoints
|
|
544
568
|
}
|
|
545
569
|
|
|
546
|
-
fn sample_native_endpoint(
|
|
547
|
-
endpoint: &str,
|
|
548
|
-
registered: &[RegisteredNode],
|
|
549
|
-
) -> Option<ProbeResult> {
|
|
570
|
+
fn sample_native_endpoint(endpoint: &str, registered: &[RegisteredNode]) -> Option<ProbeResult> {
|
|
550
571
|
let output = run_native_tmux_list_panes(endpoint)?;
|
|
551
|
-
Some(sample_native_panes(
|
|
572
|
+
Some(sample_native_panes(
|
|
573
|
+
&output,
|
|
574
|
+
endpoint,
|
|
575
|
+
registered,
|
|
576
|
+
process_snapshots,
|
|
577
|
+
))
|
|
552
578
|
}
|
|
553
579
|
|
|
554
580
|
fn sample_native_panes(
|
|
@@ -557,10 +583,21 @@ fn sample_native_panes(
|
|
|
557
583
|
registered: &[RegisteredNode],
|
|
558
584
|
snapshots: impl FnOnce(&BTreeSet<u32>) -> BTreeMap<u32, ProcessSnapshot>,
|
|
559
585
|
) -> ProbeResult {
|
|
560
|
-
let panes = output
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
586
|
+
let panes = output
|
|
587
|
+
.lines()
|
|
588
|
+
.filter_map(native_pane_fields)
|
|
589
|
+
.collect::<Vec<_>>();
|
|
590
|
+
let pids = panes
|
|
591
|
+
.iter()
|
|
592
|
+
.filter_map(|fields| native_pane_pid(fields[3]))
|
|
593
|
+
.collect::<BTreeSet<_>>();
|
|
594
|
+
let processes = if pids.is_empty() {
|
|
595
|
+
BTreeMap::new()
|
|
596
|
+
} else {
|
|
597
|
+
snapshots(&pids)
|
|
598
|
+
};
|
|
599
|
+
let nodes = panes
|
|
600
|
+
.into_iter()
|
|
564
601
|
.filter_map(|fields| parse_native_pane(fields, endpoint, registered, &processes))
|
|
565
602
|
.collect();
|
|
566
603
|
ProbeResult { nodes }
|
|
@@ -573,24 +610,36 @@ fn native_pane_fields(line: &str) -> Option<[&str; 5]> {
|
|
|
573
610
|
let pane = fields.next()?;
|
|
574
611
|
let pid = fields.next().unwrap_or_default();
|
|
575
612
|
let command = fields.next().unwrap_or_default();
|
|
576
|
-
if session.is_empty() || window.is_empty() || pane.is_empty() {
|
|
613
|
+
if session.is_empty() || window.is_empty() || pane.is_empty() {
|
|
614
|
+
return None;
|
|
615
|
+
}
|
|
577
616
|
Some([session, window, pane, pid, command])
|
|
578
617
|
}
|
|
579
618
|
|
|
580
619
|
fn native_pane_pid(pid: &str) -> Option<u32> {
|
|
581
|
-
pid.parse::<u32>()
|
|
620
|
+
pid.parse::<u32>()
|
|
621
|
+
.ok()
|
|
622
|
+
.filter(|pid| *pid > 0 && *pid <= i32::MAX as u32)
|
|
582
623
|
}
|
|
583
624
|
|
|
584
625
|
fn run_native_tmux_list_panes(endpoint: &str) -> Option<String> {
|
|
585
|
-
const FORMAT: &str =
|
|
586
|
-
|
|
626
|
+
const FORMAT: &str =
|
|
627
|
+
"#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}";
|
|
628
|
+
let flag = if Path::new(endpoint).is_absolute() {
|
|
629
|
+
"-S"
|
|
630
|
+
} else {
|
|
631
|
+
"-L"
|
|
632
|
+
};
|
|
587
633
|
let output = Command::new("tmux")
|
|
588
634
|
.arg(flag)
|
|
589
635
|
.arg(endpoint)
|
|
590
636
|
.args(["list-panes", "-a", "-F", FORMAT])
|
|
591
637
|
.output()
|
|
592
638
|
.ok()?;
|
|
593
|
-
output
|
|
639
|
+
output
|
|
640
|
+
.status
|
|
641
|
+
.success()
|
|
642
|
+
.then(|| String::from_utf8(output.stdout).ok())?
|
|
594
643
|
}
|
|
595
644
|
|
|
596
645
|
fn parse_native_pane(
|
|
@@ -643,30 +692,52 @@ struct ProcessSnapshot {
|
|
|
643
692
|
}
|
|
644
693
|
|
|
645
694
|
fn process_snapshot_command(pids: &BTreeSet<u32>) -> Command {
|
|
646
|
-
let pids = pids
|
|
695
|
+
let pids = pids
|
|
696
|
+
.iter()
|
|
697
|
+
.map(u32::to_string)
|
|
698
|
+
.collect::<Vec<_>>()
|
|
699
|
+
.join(",");
|
|
647
700
|
let mut command = Command::new("ps");
|
|
648
701
|
command.args(["-o", "pid=,ppid=,stat=,comm=", "-p", &pids]);
|
|
649
702
|
command
|
|
650
703
|
}
|
|
651
704
|
|
|
652
705
|
fn process_snapshots(pids: &BTreeSet<u32>) -> BTreeMap<u32, ProcessSnapshot> {
|
|
653
|
-
if pids.is_empty() {
|
|
654
|
-
|
|
655
|
-
|
|
706
|
+
if pids.is_empty() {
|
|
707
|
+
return BTreeMap::new();
|
|
708
|
+
}
|
|
709
|
+
let Ok(output) = process_snapshot_command(pids).output() else {
|
|
710
|
+
return BTreeMap::new();
|
|
711
|
+
};
|
|
712
|
+
if !output.status.success() {
|
|
713
|
+
return BTreeMap::new();
|
|
714
|
+
}
|
|
656
715
|
parse_process_snapshots(&output.stdout, pids)
|
|
657
716
|
}
|
|
658
717
|
|
|
659
|
-
fn parse_process_snapshots(
|
|
718
|
+
fn parse_process_snapshots(
|
|
719
|
+
bytes: &[u8],
|
|
720
|
+
requested: &BTreeSet<u32>,
|
|
721
|
+
) -> BTreeMap<u32, ProcessSnapshot> {
|
|
660
722
|
let mut snapshots = BTreeMap::<u32, ProcessSnapshot>::new();
|
|
661
723
|
let output = String::from_utf8_lossy(bytes);
|
|
662
724
|
for line in output.lines() {
|
|
663
725
|
let mut fields = line.split_whitespace();
|
|
664
|
-
let Some(pid) = fields.next().and_then(native_pane_pid) else {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
726
|
+
let Some(pid) = fields.next().and_then(native_pane_pid) else {
|
|
727
|
+
continue;
|
|
728
|
+
};
|
|
729
|
+
if !requested.contains(&pid) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
let Some(_ppid) = fields.next() else {
|
|
733
|
+
continue;
|
|
734
|
+
};
|
|
735
|
+
let Some(stat) = fields.next() else {
|
|
736
|
+
continue;
|
|
737
|
+
};
|
|
668
738
|
let zombie = stat.contains('Z');
|
|
669
|
-
snapshots
|
|
739
|
+
snapshots
|
|
740
|
+
.entry(pid)
|
|
670
741
|
.and_modify(|snapshot| snapshot.zombie |= zombie)
|
|
671
742
|
.or_insert(ProcessSnapshot { zombie });
|
|
672
743
|
}
|
|
@@ -688,11 +759,8 @@ fn probe_registered_endpoints_with_test_nodeprobe(
|
|
|
688
759
|
continue;
|
|
689
760
|
}
|
|
690
761
|
if !resolved {
|
|
691
|
-
selected_binary =
|
|
692
|
-
candidates.clone(),
|
|
693
|
-
deadline,
|
|
694
|
-
resolver_options.clone(),
|
|
695
|
-
);
|
|
762
|
+
selected_binary =
|
|
763
|
+
resolve_nodeprobe_binary(candidates.clone(), deadline, resolver_options.clone());
|
|
696
764
|
resolved = true;
|
|
697
765
|
}
|
|
698
766
|
let sampled = selected_binary.as_deref().and_then(|binary| {
|
|
@@ -868,9 +936,7 @@ fn resolve_nodeprobe_binary(
|
|
|
868
936
|
) -> Option<PathBuf> {
|
|
869
937
|
#[cfg(not(test))]
|
|
870
938
|
let _ = options;
|
|
871
|
-
if Instant::now() >= deadline
|
|
872
|
-
|| NODEPROBE_RESOLVER_ACTIVE.swap(true, Ordering::AcqRel)
|
|
873
|
-
{
|
|
939
|
+
if Instant::now() >= deadline || NODEPROBE_RESOLVER_ACTIVE.swap(true, Ordering::AcqRel) {
|
|
874
940
|
return None;
|
|
875
941
|
}
|
|
876
942
|
let (tx, rx) = mpsc::sync_channel(1);
|
|
@@ -910,10 +976,7 @@ fn resolve_nodeprobe_binary(
|
|
|
910
976
|
}
|
|
911
977
|
|
|
912
978
|
#[cfg(test)]
|
|
913
|
-
fn select_nodeprobe_binary(
|
|
914
|
-
candidates: Vec<PathBuf>,
|
|
915
|
-
deadline: Instant,
|
|
916
|
-
) -> Option<PathBuf> {
|
|
979
|
+
fn select_nodeprobe_binary(candidates: Vec<PathBuf>, deadline: Instant) -> Option<PathBuf> {
|
|
917
980
|
for candidate in candidates {
|
|
918
981
|
if Instant::now() >= deadline {
|
|
919
982
|
return None;
|
|
@@ -961,13 +1024,12 @@ fn validate_nodeprobe_candidate(path: &Path, deadline: Instant) -> Option<PathBu
|
|
|
961
1024
|
if !receipt_metadata.is_file() || receipt_metadata.len() > NODEPROBE_RECEIPT_LIMIT {
|
|
962
1025
|
return None;
|
|
963
1026
|
}
|
|
964
|
-
let receipt: NodeprobeCapabilityReceipt =
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
.ok()?;
|
|
1027
|
+
let receipt: NodeprobeCapabilityReceipt = serde_json::from_slice(&read_bounded_file(
|
|
1028
|
+
&receipt_path,
|
|
1029
|
+
NODEPROBE_RECEIPT_LIMIT,
|
|
1030
|
+
deadline,
|
|
1031
|
+
)?)
|
|
1032
|
+
.ok()?;
|
|
971
1033
|
if !valid_nodeprobe_receipt(&receipt, path) || Instant::now() >= deadline {
|
|
972
1034
|
return None;
|
|
973
1035
|
}
|
|
@@ -1227,10 +1289,7 @@ fn project_node_with_attach(
|
|
|
1227
1289
|
let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
|
|
1228
1290
|
&& observed.evidence_method.as_deref() == Some("pi_activity_channel");
|
|
1229
1291
|
let native_tmux = observed.evidence_method.as_deref() == Some("native_tmux");
|
|
1230
|
-
let activity = if observed.provider.eq_ignore_ascii_case("pi")
|
|
1231
|
-
&& !pi_channel
|
|
1232
|
-
&& !native_tmux
|
|
1233
|
-
{
|
|
1292
|
+
let activity = if observed.provider.eq_ignore_ascii_case("pi") && !pi_channel && !native_tmux {
|
|
1234
1293
|
"unknown"
|
|
1235
1294
|
} else {
|
|
1236
1295
|
valid_activity(&observed.activity)
|
|
@@ -1391,9 +1450,15 @@ mod native_ps_batch_regressions {
|
|
|
1391
1450
|
|
|
1392
1451
|
#[test]
|
|
1393
1452
|
fn eight_panes_use_one_endpoint_query() {
|
|
1394
|
-
let registered = (0..8)
|
|
1395
|
-
|
|
1396
|
-
|
|
1453
|
+
let registered = (0..8)
|
|
1454
|
+
.map(|i| registered("e1", &format!("%{i}")))
|
|
1455
|
+
.collect::<Vec<_>>();
|
|
1456
|
+
let panes = (0..8)
|
|
1457
|
+
.map(|i| format!("hp\tw\t%{i}\t{}\tpi\n", 100 + i))
|
|
1458
|
+
.collect::<String>();
|
|
1459
|
+
let rows = (0..8)
|
|
1460
|
+
.map(|i| format!("{} 1 S pi\n", 100 + i))
|
|
1461
|
+
.collect::<String>();
|
|
1397
1462
|
let mut calls = Vec::new();
|
|
1398
1463
|
let result = sample_native_panes(&panes, "e1", ®istered, |pids| {
|
|
1399
1464
|
calls.push(pids.iter().copied().collect::<Vec<_>>());
|
|
@@ -1401,23 +1466,33 @@ mod native_ps_batch_regressions {
|
|
|
1401
1466
|
});
|
|
1402
1467
|
assert_eq!(calls, vec![(100..108).collect::<Vec<_>>()]);
|
|
1403
1468
|
assert_eq!(result.nodes.len(), 8);
|
|
1404
|
-
assert!(result.nodes.iter().all(|node| node.provider == "pi"
|
|
1469
|
+
assert!(result.nodes.iter().all(|node| node.provider == "pi"
|
|
1470
|
+
&& node.activity == "working"
|
|
1471
|
+
&& node.health == "normal"));
|
|
1405
1472
|
}
|
|
1406
1473
|
|
|
1407
1474
|
#[test]
|
|
1408
1475
|
fn pid_deduplication_and_command_preserve_the_privacy_whitelist() {
|
|
1409
1476
|
let mut calls = Vec::new();
|
|
1410
|
-
let result = sample_native_panes(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1477
|
+
let result = sample_native_panes(
|
|
1478
|
+
"hp\tw\t%1\t11\tsh\nhp\tw\t%2\t011\tsh\n",
|
|
1479
|
+
"e1",
|
|
1480
|
+
&[],
|
|
1481
|
+
|pids| {
|
|
1482
|
+
calls.push(pids.iter().copied().collect::<Vec<_>>());
|
|
1483
|
+
parse_process_snapshots(b"11 1 S sh\n", pids)
|
|
1484
|
+
},
|
|
1485
|
+
);
|
|
1414
1486
|
assert_eq!(calls, vec![vec![11]]);
|
|
1415
1487
|
assert_eq!(result.nodes.len(), 2);
|
|
1416
1488
|
let pids = BTreeSet::from([22, 11]);
|
|
1417
1489
|
let command = process_snapshot_command(&pids);
|
|
1418
1490
|
assert_eq!(command.get_program(), "ps");
|
|
1419
1491
|
assert_eq!(
|
|
1420
|
-
command
|
|
1492
|
+
command
|
|
1493
|
+
.get_args()
|
|
1494
|
+
.map(|arg| arg.to_string_lossy().into_owned())
|
|
1495
|
+
.collect::<Vec<_>>(),
|
|
1421
1496
|
vec!["-o", "pid=,ppid=,stat=,comm=", "-p", "11,22"]
|
|
1422
1497
|
);
|
|
1423
1498
|
let command = process_snapshot_command(&BTreeSet::from([11]));
|
|
@@ -1431,8 +1506,14 @@ mod native_ps_batch_regressions {
|
|
|
1431
1506
|
let requested = BTreeSet::from([pid, i32::MAX as u32]);
|
|
1432
1507
|
let command = process_snapshot_command(&requested);
|
|
1433
1508
|
// Absolute ps avoids unrelated PATH-fixture mutation in parallel unit tests.
|
|
1434
|
-
let output = Command::new("/bin/ps")
|
|
1435
|
-
|
|
1509
|
+
let output = Command::new("/bin/ps")
|
|
1510
|
+
.args(command.get_args())
|
|
1511
|
+
.output()
|
|
1512
|
+
.unwrap();
|
|
1513
|
+
assert!(
|
|
1514
|
+
output.status.success(),
|
|
1515
|
+
"partial PID selection must not poison the live row"
|
|
1516
|
+
);
|
|
1436
1517
|
let snapshots = parse_process_snapshots(&output.stdout, &requested);
|
|
1437
1518
|
assert!(!snapshots.get(&pid).unwrap().zombie);
|
|
1438
1519
|
assert!(!snapshots.contains_key(&(i32::MAX as u32)));
|
|
@@ -1457,14 +1538,26 @@ mod native_ps_batch_regressions {
|
|
|
1457
1538
|
let result = sample_native_panes(panes, "e1", &[], |pids| {
|
|
1458
1539
|
parse_process_snapshots(b"22 1 Z+ dead\n11 1 S sh\n999 1 S unrelated\n33 1\n", pids)
|
|
1459
1540
|
});
|
|
1460
|
-
assert_eq!(
|
|
1461
|
-
|
|
1541
|
+
assert_eq!(
|
|
1542
|
+
result
|
|
1543
|
+
.nodes
|
|
1544
|
+
.iter()
|
|
1545
|
+
.map(|node| node.pane.as_str())
|
|
1546
|
+
.collect::<Vec<_>>(),
|
|
1547
|
+
vec!["%1", "%4"]
|
|
1548
|
+
);
|
|
1549
|
+
assert!(result
|
|
1550
|
+
.nodes
|
|
1551
|
+
.iter()
|
|
1552
|
+
.all(|node| node.activity == "idle" && node.health == "normal"));
|
|
1462
1553
|
}
|
|
1463
1554
|
|
|
1464
1555
|
#[test]
|
|
1465
1556
|
fn empty_or_invalid_pid_sets_do_not_run_ps() {
|
|
1466
1557
|
let panes = "hp\tw\t%1\t\tsh\nhp\tw\t%2\t0\tsh\nhp\tw\t%3\t1,2\tsh\nhp\tw\t%4\t4294967295\tsh\n\tw\t%5\t99\tsh\n";
|
|
1467
|
-
let result = sample_native_panes(panes, "e1", &[], |_|
|
|
1558
|
+
let result = sample_native_panes(panes, "e1", &[], |_| {
|
|
1559
|
+
panic!("no valid PID means no ps query")
|
|
1560
|
+
});
|
|
1468
1561
|
assert_eq!(result.nodes.len(), 1);
|
|
1469
1562
|
assert_eq!(result.nodes[0].pane, "%1");
|
|
1470
1563
|
assert_eq!(result.nodes[0].health, "normal");
|
|
@@ -1486,16 +1579,34 @@ mod native_ps_batch_regressions {
|
|
|
1486
1579
|
assert!(first.nodes.is_empty());
|
|
1487
1580
|
assert_eq!(second.nodes.len(), 1);
|
|
1488
1581
|
let failed = Some(first);
|
|
1489
|
-
assert_eq!(
|
|
1582
|
+
assert_eq!(
|
|
1583
|
+
project_node_with_attach(®istered[0], Some(&failed))
|
|
1584
|
+
.0
|
|
1585
|
+
.health,
|
|
1586
|
+
"unknown"
|
|
1587
|
+
);
|
|
1490
1588
|
let live = Some(second);
|
|
1491
|
-
assert_eq!(
|
|
1589
|
+
assert_eq!(
|
|
1590
|
+
project_node_with_attach(®istered[1], Some(&live))
|
|
1591
|
+
.0
|
|
1592
|
+
.health,
|
|
1593
|
+
"normal"
|
|
1594
|
+
);
|
|
1492
1595
|
}
|
|
1493
1596
|
|
|
1494
1597
|
#[test]
|
|
1495
1598
|
fn duplicate_process_rows_cannot_hide_a_zombie() {
|
|
1496
1599
|
let requested = BTreeSet::from([11]);
|
|
1497
|
-
for rows in [
|
|
1498
|
-
|
|
1600
|
+
for rows in [
|
|
1601
|
+
b"11 1 Z dead\n11 1 S sh\n".as_slice(),
|
|
1602
|
+
b"11 1 S sh\n11 1 Z dead\n".as_slice(),
|
|
1603
|
+
] {
|
|
1604
|
+
assert!(
|
|
1605
|
+
parse_process_snapshots(rows, &requested)
|
|
1606
|
+
.get(&11)
|
|
1607
|
+
.unwrap()
|
|
1608
|
+
.zombie
|
|
1609
|
+
);
|
|
1499
1610
|
}
|
|
1500
1611
|
assert!(parse_process_snapshots(b"not a process row\n", &requested).is_empty());
|
|
1501
1612
|
}
|
|
@@ -1504,14 +1615,41 @@ mod native_ps_batch_regressions {
|
|
|
1504
1615
|
fn json_and_human_keep_nine_fields_without_ps_details() {
|
|
1505
1616
|
let registered = vec![registered("e1", "%1"), registered("e1", "%2")];
|
|
1506
1617
|
let probe = Some(sample_native_panes(
|
|
1507
|
-
"hp\tw\t%1\t11\tsh\nhp\tw\t%2\t22\tpi\n",
|
|
1508
|
-
|
|
1618
|
+
"hp\tw\t%1\t11\tsh\nhp\tw\t%2\t22\tpi\n",
|
|
1619
|
+
"e1",
|
|
1620
|
+
®istered,
|
|
1621
|
+
|pids| {
|
|
1622
|
+
parse_process_snapshots(
|
|
1623
|
+
b"22 1 R PRIVATE_COMMAND_DETAIL\n11 1 S PRIVATE_COMMAND_DETAIL\n",
|
|
1624
|
+
pids,
|
|
1625
|
+
)
|
|
1626
|
+
},
|
|
1509
1627
|
));
|
|
1510
|
-
let projected = registered
|
|
1511
|
-
|
|
1628
|
+
let projected = registered
|
|
1629
|
+
.iter()
|
|
1630
|
+
.map(|node| project_node_with_attach(node, Some(&probe)))
|
|
1631
|
+
.collect::<Vec<_>>();
|
|
1632
|
+
let keys = BTreeSet::from([
|
|
1633
|
+
"name",
|
|
1634
|
+
"provider",
|
|
1635
|
+
"model",
|
|
1636
|
+
"effort",
|
|
1637
|
+
"runtime_status",
|
|
1638
|
+
"activity",
|
|
1639
|
+
"health",
|
|
1640
|
+
"session_name",
|
|
1641
|
+
"tmux_command",
|
|
1642
|
+
]);
|
|
1512
1643
|
for (node, _) in &projected {
|
|
1513
1644
|
let json = node.clone().into_json();
|
|
1514
|
-
assert_eq!(
|
|
1645
|
+
assert_eq!(
|
|
1646
|
+
json.as_object()
|
|
1647
|
+
.unwrap()
|
|
1648
|
+
.keys()
|
|
1649
|
+
.map(String::as_str)
|
|
1650
|
+
.collect::<BTreeSet<_>>(),
|
|
1651
|
+
keys
|
|
1652
|
+
);
|
|
1515
1653
|
assert_eq!(json["model"], "fixture-model");
|
|
1516
1654
|
assert_eq!(json["effort"], "high");
|
|
1517
1655
|
assert_eq!(json["runtime_status"], "running");
|
|
@@ -1571,12 +1709,21 @@ mod tests {
|
|
|
1571
1709
|
node.effort = settings.1.map(str::to_string);
|
|
1572
1710
|
for lifecycle in [None, Some("stopped")] {
|
|
1573
1711
|
node.lifecycle = lifecycle.map(str::to_string);
|
|
1574
|
-
for sample in [
|
|
1712
|
+
for sample in [
|
|
1713
|
+
None,
|
|
1714
|
+
Some(ProbeResult {
|
|
1715
|
+
nodes: vec![probe("pi_activity_channel")],
|
|
1716
|
+
}),
|
|
1717
|
+
] {
|
|
1575
1718
|
let value = project_node(&node, Some(&sample)).into_json();
|
|
1576
1719
|
assert_eq!(value["model"], json!(settings.0));
|
|
1577
1720
|
assert_eq!(value["effort"], json!(settings.1));
|
|
1578
1721
|
let human = format_brief_node(&value).unwrap();
|
|
1579
|
-
assert!(human.contains(&format!(
|
|
1722
|
+
assert!(human.contains(&format!(
|
|
1723
|
+
"model: {} effort: {}",
|
|
1724
|
+
settings.0.unwrap_or("null"),
|
|
1725
|
+
settings.1.unwrap_or("null")
|
|
1726
|
+
)));
|
|
1580
1727
|
assert_eq!(value.as_object().unwrap().len(), 9);
|
|
1581
1728
|
}
|
|
1582
1729
|
}
|
|
@@ -1606,7 +1753,12 @@ mod tests {
|
|
|
1606
1753
|
fn pi_pane_title_is_unknown_not_idle() {
|
|
1607
1754
|
let node = registered();
|
|
1608
1755
|
let observed = probe("pane_title");
|
|
1609
|
-
let projected = project_node(
|
|
1756
|
+
let projected = project_node(
|
|
1757
|
+
&node,
|
|
1758
|
+
Some(&Some(ProbeResult {
|
|
1759
|
+
nodes: vec![observed],
|
|
1760
|
+
})),
|
|
1761
|
+
);
|
|
1610
1762
|
assert_eq!(projected.runtime_status, "running");
|
|
1611
1763
|
assert_eq!(projected.activity, "unknown");
|
|
1612
1764
|
assert_eq!(projected.health, "unknown");
|
|
@@ -1616,7 +1768,12 @@ mod tests {
|
|
|
1616
1768
|
fn exact_probe_generates_quoted_window_pane_command() {
|
|
1617
1769
|
let node = registered();
|
|
1618
1770
|
let observed = probe("pi_activity_channel");
|
|
1619
|
-
let projected = project_node(
|
|
1771
|
+
let projected = project_node(
|
|
1772
|
+
&node,
|
|
1773
|
+
Some(&Some(ProbeResult {
|
|
1774
|
+
nodes: vec![observed],
|
|
1775
|
+
})),
|
|
1776
|
+
);
|
|
1620
1777
|
assert_eq!(projected.activity, "idle");
|
|
1621
1778
|
assert_eq!(
|
|
1622
1779
|
projected.tmux_command.as_deref(),
|
|
@@ -1639,7 +1796,12 @@ mod tests {
|
|
|
1639
1796
|
let mut node = registered();
|
|
1640
1797
|
node.lifecycle = Some("stopped".to_string());
|
|
1641
1798
|
let observed = probe("pi_activity_channel");
|
|
1642
|
-
let projected = project_node(
|
|
1799
|
+
let projected = project_node(
|
|
1800
|
+
&node,
|
|
1801
|
+
Some(&Some(ProbeResult {
|
|
1802
|
+
nodes: vec![observed],
|
|
1803
|
+
})),
|
|
1804
|
+
);
|
|
1643
1805
|
assert_eq!(projected.runtime_status, "unknown");
|
|
1644
1806
|
assert!(projected.tmux_command.is_none());
|
|
1645
1807
|
}
|
|
@@ -1663,7 +1825,12 @@ mod tests {
|
|
|
1663
1825
|
let node = registered();
|
|
1664
1826
|
let mut observed = probe("pi_activity_channel");
|
|
1665
1827
|
observed.pane = "%9".to_string();
|
|
1666
|
-
let projected = project_node(
|
|
1828
|
+
let projected = project_node(
|
|
1829
|
+
&node,
|
|
1830
|
+
Some(&Some(ProbeResult {
|
|
1831
|
+
nodes: vec![observed],
|
|
1832
|
+
})),
|
|
1833
|
+
);
|
|
1667
1834
|
assert_eq!(projected.runtime_status, "unknown");
|
|
1668
1835
|
}
|
|
1669
1836
|
|