@team-agent/installer 0.5.111 → 0.5.112
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -743
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +31 -275
- package/crates/team-agent/src/cli/mod.rs +2 -9
- package/crates/team-agent/src/cli/spec.rs +0 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/types.rs +4 -108
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +0 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//!
|
|
2
2
|
//! `team.db` schema 初始化(真相源 `message_store/schema.py` + `schema_migration.py`)。
|
|
3
3
|
//!
|
|
4
|
-
//! slice 1:fresh DB schema ——
|
|
4
|
+
//! slice 1:fresh DB schema —— 7 表 DDL、6 索引、ALTER 式列迁移、agent_health 重建、
|
|
5
5
|
//! WAL/busy_timeout pragmas、`user_version = SCHEMA_VERSION`。
|
|
6
6
|
//! slice 2(待做):列**顺序**漂移的整表 rebuild(`ensure_table_layout`/`_rebuild_tables`)
|
|
7
7
|
//! 与 `schema_diagnosis`,验 legacy_team_db_fixture + schema_migration 契约。
|
|
@@ -15,14 +15,13 @@ use crate::db::DbError;
|
|
|
15
15
|
/// `schema.py:90`。
|
|
16
16
|
pub const SCHEMA_VERSION: i64 = 5;
|
|
17
17
|
|
|
18
|
-
///
|
|
18
|
+
/// 7 张表的 DDL(逐字照搬 `schema.py:initialize_schema` 的内联建表;含 `if not exists`)。
|
|
19
19
|
/// 顺序与 Python 一致(leader_notification_log 在 ensure 块后创建)。
|
|
20
20
|
const CREATE_MESSAGES: &str = "create table if not exists messages (\n message_id text primary key,\n owner_team_id text,\n task_id text,\n sender text,\n recipient text,\n reply_to text,\n requires_ack integer,\n status text,\n content text,\n presentation text not null default '{\"sink\":\"leader\",\"class\":\"message\"}',\n artifact_refs text,\n created_at text,\n updated_at text,\n delivered_at text,\n acknowledged_at text,\n error text,\n delivery_attempts integer not null default 0\n )";
|
|
21
21
|
const CREATE_RESULTS: &str = "create table if not exists results (\n result_id text primary key,\n owner_team_id text,\n task_id text not null,\n agent_id text not null,\n envelope text not null,\n status text not null,\n created_at text not null\n )";
|
|
22
22
|
const CREATE_SCHEDULED_EVENTS: &str = "create table if not exists scheduled_events (\n id integer primary key,\n owner_team_id text,\n due_at text not null,\n target text not null,\n kind text not null,\n payload_json text not null,\n status text not null,\n created_at text not null,\n fired_at text,\n result_json text\n )";
|
|
23
23
|
const CREATE_DELIVERY_TOKENS: &str = "create table if not exists delivery_tokens (\n message_id text primary key,\n unique_token text not null,\n injected_at text not null,\n visible_at text,\n consumed_at text,\n failed_at text,\n failure_reason text\n )";
|
|
24
24
|
const CREATE_AGENT_HEALTH: &str = "create table if not exists agent_health (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
|
|
25
|
-
const CREATE_PEER_ALLOWLIST: &str = "create table if not exists peer_allowlist (\n a text not null,\n b text not null,\n created_at text not null,\n primary key (a, b)\n )";
|
|
26
25
|
const CREATE_RESULT_WATCHERS: &str = "create table if not exists result_watchers (\n watcher_id text primary key,\n owner_team_id text,\n task_id text,\n agent_id text,\n message_id text,\n leader_id text not null,\n recipient text,\n status text not null,\n created_at text not null,\n completed_at text,\n result_id text,\n notified_message_id text,\n error text\n )";
|
|
27
26
|
const CREATE_LEADER_NOTIFICATION_LOG: &str = "create table if not exists leader_notification_log (\n result_id text not null,\n owner_team_id text not null default '',\n owner_epoch integer not null default 0,\n leader_session_uuid text,\n notified_message_id text not null,\n notified_at text not null,\n leader_pane_id_at_notify text,\n envelope_content_hash text,\n primary key (result_id, owner_team_id, owner_epoch)\n )";
|
|
28
27
|
const CREATE_AGENT_HEALTH_NEW: &str = "create table agent_health_new (\n owner_team_id text,\n agent_id text not null,\n status text not null,\n last_output_at text,\n context_usage_pct integer,\n current_task_id text,\n updated_at text not null,\n unique(owner_team_id, agent_id)\n )";
|
|
@@ -96,7 +95,6 @@ const AGENT_HEALTH_COLUMNS: &[&str] = &[
|
|
|
96
95
|
"current_task_id",
|
|
97
96
|
"updated_at",
|
|
98
97
|
];
|
|
99
|
-
const PEER_ALLOWLIST_COLUMNS: &[&str] = &["a", "b", "created_at"];
|
|
100
98
|
const RESULT_WATCHER_COLUMNS: &[&str] = &[
|
|
101
99
|
"owner_team_id",
|
|
102
100
|
"watcher_id",
|
|
@@ -249,7 +247,6 @@ pub fn initialize_schema(
|
|
|
249
247
|
CREATE_SCHEDULED_EVENTS,
|
|
250
248
|
CREATE_DELIVERY_TOKENS,
|
|
251
249
|
CREATE_AGENT_HEALTH,
|
|
252
|
-
CREATE_PEER_ALLOWLIST,
|
|
253
250
|
CREATE_RESULT_WATCHERS,
|
|
254
251
|
] {
|
|
255
252
|
tx.execute(ddl, [])?;
|
|
@@ -293,7 +290,6 @@ pub fn initialize_schema(
|
|
|
293
290
|
)?;
|
|
294
291
|
ensure_table_columns(&tx, "delivery_tokens", DELIVERY_TOKEN_COLUMNS, &[])?;
|
|
295
292
|
migrate_agent_health_owner_team_id(&tx)?;
|
|
296
|
-
ensure_table_columns(&tx, "peer_allowlist", PEER_ALLOWLIST_COLUMNS, &[])?;
|
|
297
293
|
ensure_table_columns(
|
|
298
294
|
&tx,
|
|
299
295
|
"result_watchers",
|
|
@@ -333,7 +329,28 @@ mod tests {
|
|
|
333
329
|
conn
|
|
334
330
|
}
|
|
335
331
|
|
|
336
|
-
|
|
332
|
+
#[test]
|
|
333
|
+
fn retired_peer_allowlist_is_neither_created_nor_managed() {
|
|
334
|
+
let conn = fresh();
|
|
335
|
+
assert!(table_layout(&conn, "peer_allowlist").unwrap().is_empty());
|
|
336
|
+
// Legacy residue may have an obsolete layout. Initialization must leave
|
|
337
|
+
// its data alone, not rebuild/drop it or require it for schema health.
|
|
338
|
+
conn.execute_batch(
|
|
339
|
+
"create table peer_allowlist (legacy_payload text);\n insert into peer_allowlist values ('preserve-this-row');",
|
|
340
|
+
)
|
|
341
|
+
.unwrap();
|
|
342
|
+
initialize_schema(&conn, None).unwrap();
|
|
343
|
+
assert!(crate::db::migration::ensure_table_layout(&conn, SCHEMA_VERSION, None)
|
|
344
|
+
.unwrap()
|
|
345
|
+
.is_empty());
|
|
346
|
+
assert_eq!(table_layout(&conn, "peer_allowlist").unwrap(), ["legacy_payload"]);
|
|
347
|
+
let value: String = conn.query_row("select legacy_payload from peer_allowlist", [], |row| row.get(0)).unwrap();
|
|
348
|
+
assert_eq!(value, "preserve-this-row");
|
|
349
|
+
let version: i64 = conn.query_row("pragma user_version", [], |row| row.get(0)).unwrap();
|
|
350
|
+
assert_eq!(version, 5);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fresh managed schema, excluding retired legacy tables.
|
|
337
354
|
#[test]
|
|
338
355
|
fn fresh_schema_matches_python_sqlite_master() {
|
|
339
356
|
let conn = fresh();
|
|
@@ -356,7 +373,6 @@ mod tests {
|
|
|
356
373
|
("idx_scheduled_events_owner_team_id", "index"),
|
|
357
374
|
("leader_notification_log", "table"),
|
|
358
375
|
("messages", "table"),
|
|
359
|
-
("peer_allowlist", "table"),
|
|
360
376
|
("result_watchers", "table"),
|
|
361
377
|
("results", "table"),
|
|
362
378
|
("scheduled_events", "table"),
|
|
@@ -419,9 +435,9 @@ mod tests {
|
|
|
419
435
|
assert_eq!(idx, 6, "二次 init 后 6 个 named index 仍在");
|
|
420
436
|
}
|
|
421
437
|
|
|
422
|
-
//
|
|
438
|
+
// Managed tables: (name, type, notnull, dflt, pk).
|
|
423
439
|
#[test]
|
|
424
|
-
fn
|
|
440
|
+
fn per_column_table_info_managed_tables() {
|
|
425
441
|
type Col = (&'static str, &'static str, i64, Option<&'static str>, i64);
|
|
426
442
|
let golden: &[(&str, &[Col])] = &[
|
|
427
443
|
(
|
|
@@ -503,14 +519,6 @@ mod tests {
|
|
|
503
519
|
("updated_at", "TEXT", 1, None, 0),
|
|
504
520
|
],
|
|
505
521
|
),
|
|
506
|
-
(
|
|
507
|
-
"peer_allowlist",
|
|
508
|
-
&[
|
|
509
|
-
("a", "TEXT", 1, None, 1),
|
|
510
|
-
("b", "TEXT", 1, None, 2),
|
|
511
|
-
("created_at", "TEXT", 1, None, 0),
|
|
512
|
-
],
|
|
513
|
-
),
|
|
514
522
|
(
|
|
515
523
|
"result_watchers",
|
|
516
524
|
&[
|
|
@@ -582,7 +590,7 @@ mod tests {
|
|
|
582
590
|
];
|
|
583
591
|
let got: Vec<(&str, &str)> = idx.iter().map(|(n, s)| (n.as_str(), s.as_str())).collect();
|
|
584
592
|
assert_eq!(got, want);
|
|
585
|
-
//
|
|
593
|
+
// 6 个 sqlite_autoindex(每个有 pk/unique 约束的表各一)。
|
|
586
594
|
let auto: Vec<String> = conn
|
|
587
595
|
.prepare(
|
|
588
596
|
"select name from sqlite_master where type='index' and sql is null order by name",
|
|
@@ -599,7 +607,6 @@ mod tests {
|
|
|
599
607
|
"sqlite_autoindex_delivery_tokens_1",
|
|
600
608
|
"sqlite_autoindex_leader_notification_log_1",
|
|
601
609
|
"sqlite_autoindex_messages_1",
|
|
602
|
-
"sqlite_autoindex_peer_allowlist_1",
|
|
603
610
|
"sqlite_autoindex_result_watchers_1",
|
|
604
611
|
"sqlite_autoindex_results_1",
|
|
605
612
|
]
|
|
@@ -17,30 +17,6 @@ use super::{
|
|
|
17
17
|
OwnerBindResult, TeamOwner,
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
// ── leader::identity — leader_identity / 身份上下文 ──
|
|
21
|
-
|
|
22
|
-
/// `leader_identity`(card §47;`__init__.py:355`)。`team-agent identity` 入口。
|
|
23
|
-
/// 返回 uuid_prefix + 身份字段(JSON dict,CLI 直出)。
|
|
24
|
-
pub fn leader_identity(workspace: &Path, team: Option<&str>) -> Result<Value, LeaderError> {
|
|
25
|
-
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
26
|
-
let identity = leader_identity_context(workspace, team, Some(&state))?;
|
|
27
|
-
Ok(json!({
|
|
28
|
-
"ok": true,
|
|
29
|
-
"uuid_prefix": prefix(identity.leader_session_uuid.as_str(), 12),
|
|
30
|
-
"machine_fingerprint": identity.machine_fingerprint,
|
|
31
|
-
"workspace_abspath": identity.workspace_abspath.to_string_lossy(),
|
|
32
|
-
"os_user": identity.os_user,
|
|
33
|
-
"team_id": identity.team_id.as_str(),
|
|
34
|
-
"current_pane_id": std::env::var("TEAM_AGENT_LEADER_PANE_ID")
|
|
35
|
-
.ok()
|
|
36
|
-
.filter(|v| !v.is_empty())
|
|
37
|
-
.or_else(|| std::env::var("TMUX_PANE").ok().filter(|v| !v.is_empty())),
|
|
38
|
-
"last_seen_at": get_path_str(&state, &["leader_receiver", "attached_at"])
|
|
39
|
-
.or_else(|| get_path_str(&state, &["leader_receiver", "last_seen_at"])),
|
|
40
|
-
"source": serde_json::to_value(identity.leader_session_uuid_source)?,
|
|
41
|
-
}))
|
|
42
|
-
}
|
|
43
|
-
|
|
44
20
|
/// `_leader_identity_context`(`__init__.py:192`)。派生 leader 身份上下文(override / state / derive)。
|
|
45
21
|
pub fn leader_identity_context(
|
|
46
22
|
workspace: &Path,
|
|
@@ -353,23 +353,3 @@ fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
|
|
|
353
353
|
"non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4"
|
|
354
354
|
);
|
|
355
355
|
}
|
|
356
|
-
|
|
357
|
-
// C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
|
|
358
|
-
// :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
|
|
359
|
-
// TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
|
|
360
|
-
// the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
|
|
361
|
-
// Rust reads attached_at only.)
|
|
362
|
-
#[test]
|
|
363
|
-
#[serial_test::serial(env)]
|
|
364
|
-
fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
|
|
365
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
366
|
-
let _e = EnvGuard::apply(&[
|
|
367
|
-
("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
|
|
368
|
-
("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
|
|
369
|
-
("TMUX_PANE", None),
|
|
370
|
-
]);
|
|
371
|
-
let ws = p2_temp_ws("c3_pane_env");
|
|
372
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
373
|
-
assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
|
|
374
|
-
"golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
|
|
375
|
-
}
|
|
@@ -23,38 +23,6 @@ fn leader_identity_context_derives_when_no_override_no_state() {
|
|
|
23
23
|
assert_eq!(id.leader_session_uuid.as_str().len(), 32);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
|
|
27
|
-
// 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
|
|
28
|
-
// 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
|
|
29
|
-
#[test]
|
|
30
|
-
#[serial_test::serial(env)]
|
|
31
|
-
fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
|
|
32
|
-
let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
|
|
33
|
-
std::fs::create_dir_all(&ws).unwrap();
|
|
34
|
-
// 空 state → context 走 derive(无 override/无 state uuid)。
|
|
35
|
-
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
36
|
-
let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
|
|
37
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
38
|
-
assert_eq!(v["ok"], serde_json::json!(true));
|
|
39
|
-
// uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
|
|
40
|
-
assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
|
|
41
|
-
// 其余身份字段与 context 字节一致。
|
|
42
|
-
assert_eq!(
|
|
43
|
-
v["machine_fingerprint"].as_str().unwrap(),
|
|
44
|
-
ctx.machine_fingerprint
|
|
45
|
-
);
|
|
46
|
-
assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
|
|
47
|
-
assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
|
|
48
|
-
// source == 派生侧 "derived"(无 override → 不是 "override"/"env")。
|
|
49
|
-
assert_eq!(v["source"], serde_json::json!("derived"));
|
|
50
|
-
assert_eq!(
|
|
51
|
-
ctx.leader_session_uuid_source,
|
|
52
|
-
LeaderSessionUuidSource::Derived
|
|
53
|
-
);
|
|
54
|
-
// CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null。
|
|
55
|
-
assert_eq!(v["last_seen_at"], serde_json::Value::Null);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
26
|
// =====================================================================
|
|
59
27
|
// 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
|
|
60
28
|
// =====================================================================
|
|
@@ -398,61 +398,6 @@ fn claim_lease_no_incident_caller_not_live_refused_under_n_rn3_any_live_pane() {
|
|
|
398
398
|
assert_eq!(r.reason, Some(LeaseReason::CallerPaneNotLive));
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
// ── 14b. leader_identity dict — workspace_abspath key (golden __init__.py:363) ──
|
|
402
|
-
//
|
|
403
|
-
// LOCK→RED: leader_identity IS implemented (owner_bind.rs:22) but its dict
|
|
404
|
-
// OMITS the golden `workspace_abspath` key (probe_lid.py shows it sits between
|
|
405
|
-
// machine_fingerprint and os_user). This pins the missing key so the porter
|
|
406
|
-
// must add it. Fails today (key absent → null) = RED against the omission.
|
|
407
|
-
#[test]
|
|
408
|
-
#[serial_test::serial(env)]
|
|
409
|
-
fn leader_identity_dict_includes_workspace_abspath_key() {
|
|
410
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
411
|
-
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
412
|
-
let ws = p2_temp_ws("lid_wsabs");
|
|
413
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
414
|
-
let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
415
|
-
// golden key present + equals the resolved workspace abspath from context.
|
|
416
|
-
assert!(
|
|
417
|
-
v.get("workspace_abspath").is_some(),
|
|
418
|
-
"golden leader_identity dict carries 'workspace_abspath' (probe_lid.py)"
|
|
419
|
-
);
|
|
420
|
-
assert_eq!(
|
|
421
|
-
v["workspace_abspath"].as_str().unwrap(),
|
|
422
|
-
ctx.workspace_abspath.to_string_lossy(),
|
|
423
|
-
"workspace_abspath must equal the context's resolved abspath"
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// LOCK — leader_identity key ORDER & full key set (golden probe_lid.py):
|
|
428
|
-
// [ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user, team_id,
|
|
429
|
-
// current_pane_id, last_seen_at, source]. Pins the 9-key surface CLI emits.
|
|
430
|
-
#[test]
|
|
431
|
-
#[serial_test::serial(env)]
|
|
432
|
-
fn leader_identity_dict_has_exact_nine_golden_keys() {
|
|
433
|
-
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
434
|
-
let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
|
|
435
|
-
let ws = p2_temp_ws("lid_keys");
|
|
436
|
-
let v = leader_identity(&ws, None).unwrap();
|
|
437
|
-
let obj = v.as_object().expect("leader_identity → JSON object");
|
|
438
|
-
for key in [
|
|
439
|
-
"ok",
|
|
440
|
-
"uuid_prefix",
|
|
441
|
-
"machine_fingerprint",
|
|
442
|
-
"workspace_abspath",
|
|
443
|
-
"os_user",
|
|
444
|
-
"team_id",
|
|
445
|
-
"current_pane_id",
|
|
446
|
-
"last_seen_at",
|
|
447
|
-
"source",
|
|
448
|
-
] {
|
|
449
|
-
assert!(
|
|
450
|
-
obj.contains_key(key),
|
|
451
|
-
"golden leader_identity dict must carry '{key}'"
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
401
|
// ═════════════════════════════════════════════════════════════════════════
|
|
457
402
|
// WAVE-2 Lane B — leader-lease DIVERGENCE round (adversarial review @ 2cd71ce).
|
|
458
403
|
// The lease gate is green but NOT byte-parity. Golden: leader/__init__.py
|
|
@@ -21,7 +21,6 @@ use std::sync::Mutex;
|
|
|
21
21
|
|
|
22
22
|
use serde_json::{json, Value};
|
|
23
23
|
use serial_test::serial;
|
|
24
|
-
use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
|
|
25
24
|
use team_agent::lifecycle::{
|
|
26
25
|
classify_restart_plan, restart_with_transport, RestartReport, ResumeDecision, StartMode,
|
|
27
26
|
};
|
|
@@ -448,48 +447,7 @@ fn restart_refuses_first_send_at_null_running_resumable_session_incomplete_when_
|
|
|
448
447
|
}
|
|
449
448
|
|
|
450
449
|
#[test]
|
|
451
|
-
|
|
452
|
-
#[serial(env)]
|
|
453
|
-
fn wait_ready_reports_session_capture_incomplete_for_running_resumable_worker_without_session() {
|
|
454
|
-
let fixture = RestartFixture::new("wait-ready-session-incomplete", "codex");
|
|
455
|
-
seed_running_state_without_session(&fixture.team, "codex", &fixture.spawn_cwd);
|
|
456
|
-
let mut state = load_runtime_state(&fixture.team).unwrap();
|
|
457
|
-
let mcp_config = fixture.team.join(".team/runtime/worker_a.mcp.json");
|
|
458
|
-
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
459
|
-
std::fs::write(&mcp_config, "{}").unwrap();
|
|
460
|
-
state["agents"]["worker_a"]["pane_id"] = json!("%1");
|
|
461
|
-
state["agents"]["worker_a"]["mcp_config"] = json!(mcp_config.to_string_lossy().to_string());
|
|
462
|
-
save_runtime_state(&fixture.team, &state).unwrap();
|
|
463
|
-
|
|
464
|
-
let out = json_result(
|
|
465
|
-
cmd_wait_ready(&WaitReadyArgs {
|
|
466
|
-
workspace: fixture.team.clone(),
|
|
467
|
-
timeout: 0.0,
|
|
468
|
-
json: true,
|
|
469
|
-
team: None,
|
|
470
|
-
})
|
|
471
|
-
.expect("wait-ready should return structured JSON"),
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
assert_eq!(
|
|
475
|
-
out["ok"],
|
|
476
|
-
json!(false),
|
|
477
|
-
"wait-ready cannot report ready while a running Codex/Claude worker is missing captured session context; out={out}"
|
|
478
|
-
);
|
|
479
|
-
assert_eq!(
|
|
480
|
-
out["reason"],
|
|
481
|
-
json!("session_capture_incomplete"),
|
|
482
|
-
"readiness reason must distinguish missing resumable session from generic workers_not_ready; out={out}"
|
|
483
|
-
);
|
|
484
|
-
assert!(
|
|
485
|
-
out["readiness"]["session_capture_incomplete"].as_bool() == Some(true)
|
|
486
|
-
&& out["readiness"]["all_resumable_have_session"].as_bool() == Some(false),
|
|
487
|
-
"readiness truth table must include all_resumable_have_session and session_capture_incomplete; out={out}"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
#[test]
|
|
492
|
-
fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completeness() {
|
|
450
|
+
fn status_and_quick_start_surfaces_include_session_capture_completeness() {
|
|
493
451
|
let diagnose =
|
|
494
452
|
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/cli/diagnose.rs"))
|
|
495
453
|
.unwrap();
|
|
@@ -500,7 +458,7 @@ fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completene
|
|
|
500
458
|
.unwrap();
|
|
501
459
|
|
|
502
460
|
for (name, source) in [
|
|
503
|
-
("
|
|
461
|
+
("status readiness formula", diagnose.as_str()),
|
|
504
462
|
("status --json", status.as_str()),
|
|
505
463
|
("quick-start readiness", launch.as_str()),
|
|
506
464
|
("quick-start --json", adapters.as_str()),
|
|
@@ -945,13 +903,6 @@ fn read_events(team: &Path) -> String {
|
|
|
945
903
|
std::fs::read_to_string(team.join(".team/logs/events.jsonl")).unwrap_or_default()
|
|
946
904
|
}
|
|
947
905
|
|
|
948
|
-
fn json_result(result: team_agent::cli::CmdResult) -> Value {
|
|
949
|
-
match result.output {
|
|
950
|
-
CmdOutput::Json(value) => value,
|
|
951
|
-
other => panic!("expected JSON command output, got {other:?}"),
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
906
|
fn assert_no_agent_attribution_ambiguous<const N: usize>(
|
|
956
907
|
state: &Value,
|
|
957
908
|
expected_agents: [&str; N],
|
|
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
|
|
|
23
23
|
|
|
24
24
|
use serde_json::{json, Value};
|
|
25
25
|
use serial_test::serial;
|
|
26
|
-
use team_agent::cli::{
|
|
26
|
+
use team_agent::cli::{cmd_doctor, emit, CmdOutput, DoctorArgs};
|
|
27
27
|
use team_agent::event_log::EventLog;
|
|
28
28
|
use team_agent::message_store::MessageStore;
|
|
29
29
|
use team_agent::state::persist::save_runtime_state;
|
|
@@ -334,11 +334,10 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
|
|
|
334
334
|
env.scrub_tmux();
|
|
335
335
|
env.assert_no_real_tmux();
|
|
336
336
|
let workspace = env.workspace("red6");
|
|
337
|
-
let spec = write_invalid_validate_spec(&workspace, &marker);
|
|
338
337
|
let mut combined = String::new();
|
|
339
338
|
for json_mode in [true, false] {
|
|
340
339
|
let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
|
|
341
|
-
command.arg("
|
|
340
|
+
command.arg("profile").arg(credential_url(&marker)).arg("synthetic-profile");
|
|
342
341
|
if json_mode {
|
|
343
342
|
command.arg("--json");
|
|
344
343
|
}
|
|
@@ -351,7 +350,7 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
|
|
|
351
350
|
.expect("run top-level CLI error boundary");
|
|
352
351
|
assert!(
|
|
353
352
|
!output.status.success(),
|
|
354
|
-
"invalid
|
|
353
|
+
"invalid profile subcommand must reach CLI error"
|
|
355
354
|
);
|
|
356
355
|
combined.push_str(&String::from_utf8_lossy(&output.stdout));
|
|
357
356
|
combined.push_str(&String::from_utf8_lossy(&output.stderr));
|
|
@@ -391,15 +390,30 @@ fn red7_pane_and_profile_scrubbers_converge_without_losing_existing_policy() {
|
|
|
391
390
|
let env = hermetic_guard::HermeticTestEnv::enter("redact-red7");
|
|
392
391
|
let pane_policy_preserved = pane_excerpt_uses_shared_redaction(&env, &marker);
|
|
393
392
|
let team = write_profile_fixture(env.root(), server.base_url(), &profile_token);
|
|
394
|
-
let result =
|
|
393
|
+
let result = cmd_doctor(&DoctorArgs {
|
|
394
|
+
spec: Some(team.join("TEAM.md")),
|
|
395
|
+
workspace: team,
|
|
396
|
+
gate: None,
|
|
397
|
+
comms: false,
|
|
398
|
+
team: None,
|
|
399
|
+
fix: false,
|
|
400
|
+
fix_schema: false,
|
|
401
|
+
cleanup_orphans: false,
|
|
402
|
+
confirm: false,
|
|
403
|
+
json: true,
|
|
404
|
+
})
|
|
395
405
|
.expect("profile smoke returns a diagnostic report");
|
|
396
406
|
let report = match result.output {
|
|
397
407
|
CmdOutput::Json(value) => value,
|
|
398
|
-
other => panic!("expected JSON
|
|
408
|
+
other => panic!("expected JSON doctor output, got {other:?}"),
|
|
399
409
|
};
|
|
400
|
-
let
|
|
401
|
-
let
|
|
402
|
-
|
|
410
|
+
let report_text = report.to_string();
|
|
411
|
+
let text = report
|
|
412
|
+
.get("profile_smoke")
|
|
413
|
+
.unwrap_or_else(|| panic!("doctor must expose profile_smoke; report={report}"))
|
|
414
|
+
.to_string();
|
|
415
|
+
let profile_policy_preserved = !report_text.contains(&marker)
|
|
416
|
+
&& !report_text.contains(&profile_token)
|
|
403
417
|
&& !text.contains(tail_sentinel)
|
|
404
418
|
&& text.contains(REDACTED)
|
|
405
419
|
&& text.contains("proxy.invalid:8443/path")
|
|
@@ -555,20 +569,6 @@ fn run_mcp_status_frames(
|
|
|
555
569
|
.collect()
|
|
556
570
|
}
|
|
557
571
|
|
|
558
|
-
fn write_invalid_validate_spec(workspace: &Path, marker: &str) -> PathBuf {
|
|
559
|
-
let path = workspace.join("invalid-provider.spec.yaml");
|
|
560
|
-
fs::write(
|
|
561
|
-
&path,
|
|
562
|
-
format!(
|
|
563
|
-
"version: 1\nteam:\n id: current\n name: current\n session_name: team-redaction-contract\n workspace: \"{}\"\nleader:\n provider: \"{}\"\nagents:\n - id: worker\n provider: fake\n model: fake\n role: Worker\n window: worker\ntasks: []\n",
|
|
564
|
-
workspace.display(),
|
|
565
|
-
credential_url(marker)
|
|
566
|
-
),
|
|
567
|
-
)
|
|
568
|
-
.unwrap();
|
|
569
|
-
path
|
|
570
|
-
}
|
|
571
|
-
|
|
572
572
|
fn read_cli_error_logs(workspace: &Path) -> String {
|
|
573
573
|
let dir = workspace.join(".team/logs");
|
|
574
574
|
fs::read_dir(dir)
|
|
@@ -92,15 +92,10 @@ const SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART: &str = r#"> You are in /private/
|
|
|
92
92
|
│ directory: /private/tmp/…/teamdir │
|
|
93
93
|
╰───────────────────────────────────────╯
|
|
94
94
|
|
|
95
|
-
|
|
96
95
|
› Find and fix a bug in @filename
|
|
97
96
|
|
|
98
97
|
gpt-5.5 default · /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/te…
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
99
|
"#;
|
|
105
100
|
|
|
106
101
|
fn tmp_dir(tag: &str) -> TempWorkspace {
|
|
@@ -419,8 +414,7 @@ fn contract_a_own_workspace_trust_is_actionable_not_ready_and_foreign_is_pending
|
|
|
419
414
|
}
|
|
420
415
|
|
|
421
416
|
#[test]
|
|
422
|
-
fn
|
|
423
|
-
use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
|
|
417
|
+
fn contract_a_runtime_readiness_is_false_while_actionable_trust_remains() {
|
|
424
418
|
use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
|
|
425
419
|
|
|
426
420
|
assert_eq!(
|
|
@@ -429,67 +423,47 @@ fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains
|
|
|
429
423
|
"fixture sanity: probe5 pane text is an actionable own-workspace trust prompt"
|
|
430
424
|
);
|
|
431
425
|
|
|
432
|
-
let ws = tmp_dir("a-
|
|
426
|
+
let ws = tmp_dir("a-runtime-readiness-trust");
|
|
433
427
|
let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
|
|
434
428
|
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
435
429
|
std::fs::write(&mcp_config, "{}").unwrap();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
430
|
+
// Keep every other readiness gate satisfied; an unattached leader would
|
|
431
|
+
// make ready=false independently of the trust prompt under test.
|
|
432
|
+
let mut state = json!({
|
|
433
|
+
"session_name": "team-subprep",
|
|
434
|
+
"tmux_session_present": true,
|
|
435
|
+
"leader_receiver": {"status": "attached", "pane_id": "%9"},
|
|
436
|
+
"agents": {
|
|
437
|
+
"w1": {
|
|
438
|
+
"provider": "codex",
|
|
439
|
+
"status": "awaiting_trust_prompt",
|
|
440
|
+
"startup_prompt_status": "awaiting_trust_prompt",
|
|
441
|
+
"startup_prompts": "awaiting_trust_prompt",
|
|
442
|
+
"pane_id": "%7",
|
|
443
|
+
"window": "w1",
|
|
444
|
+
"mcp_config": mcp_config.to_string_lossy().to_string(),
|
|
445
|
+
"task_prompt_delivered": true
|
|
451
446
|
}
|
|
452
|
-
}
|
|
453
|
-
)
|
|
454
|
-
.unwrap();
|
|
455
|
-
|
|
456
|
-
let result = cmd_wait_ready(&WaitReadyArgs {
|
|
457
|
-
workspace: ws.to_path_buf(),
|
|
458
|
-
timeout: 0.0,
|
|
459
|
-
json: true,
|
|
460
|
-
team: None,
|
|
461
|
-
})
|
|
462
|
-
.expect("wait-ready SUT should return shaped JSON");
|
|
463
|
-
let value = match result.output {
|
|
464
|
-
CmdOutput::Json(value) => value,
|
|
465
|
-
other => panic!("wait-ready --json must return JSON output, got {other:?}"),
|
|
466
|
-
};
|
|
447
|
+
}
|
|
448
|
+
});
|
|
467
449
|
|
|
450
|
+
let value = crate::cli::diagnose::wait_readiness(&state);
|
|
468
451
|
assert_eq!(
|
|
469
|
-
value["
|
|
470
|
-
json!(false),
|
|
471
|
-
"Contract A: quick-start/wait-ready readiness must not report ok=true while actionable trust remains; got {value}"
|
|
472
|
-
);
|
|
473
|
-
assert_eq!(
|
|
474
|
-
value["status"],
|
|
475
|
-
json!("pending"),
|
|
476
|
-
"Contract A: actionable trust prompt should keep readiness pending, not ready; got {value}"
|
|
477
|
-
);
|
|
478
|
-
assert_eq!(
|
|
479
|
-
value["reason"],
|
|
480
|
-
json!("awaiting_trust_prompt"),
|
|
481
|
-
"Contract A: not-ready reason must identify the startup trust boundary; got {value}"
|
|
482
|
-
);
|
|
483
|
-
assert_eq!(
|
|
484
|
-
value["readiness"]["awaiting_trust_prompt"],
|
|
452
|
+
value["awaiting_trust_prompt"],
|
|
485
453
|
json!(true),
|
|
486
454
|
"Contract A: ready logic must surface awaiting_trust_prompt=true; got {value}"
|
|
487
455
|
);
|
|
488
456
|
assert_eq!(
|
|
489
|
-
value["
|
|
457
|
+
value["ready"],
|
|
490
458
|
json!(false),
|
|
491
459
|
"Contract A: ready must stay false until the actionable trust prompt is cleared; got {value}"
|
|
492
460
|
);
|
|
461
|
+
state["agents"]["w1"]["status"] = json!("running");
|
|
462
|
+
state["agents"]["w1"]["startup_prompt_status"] = json!("complete");
|
|
463
|
+
state["agents"]["w1"]["startup_prompts"] = json!("complete");
|
|
464
|
+
let cleared = crate::cli::diagnose::wait_readiness(&state);
|
|
465
|
+
assert_eq!(cleared["awaiting_trust_prompt"], json!(false));
|
|
466
|
+
assert_eq!(cleared["ready"], json!(true), "positive control: {cleared}");
|
|
493
467
|
}
|
|
494
468
|
|
|
495
469
|
#[test]
|
|
@@ -67,7 +67,6 @@ pub mod delivery;
|
|
|
67
67
|
pub mod helpers;
|
|
68
68
|
pub mod leader_channel;
|
|
69
69
|
pub mod leader_receiver;
|
|
70
|
-
pub mod peers;
|
|
71
70
|
pub mod persist;
|
|
72
71
|
pub mod presentation;
|
|
73
72
|
pub mod results;
|
|
@@ -96,7 +95,6 @@ pub use leader_receiver::{
|
|
|
96
95
|
mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
97
96
|
send_to_leader_receiver_with_presentation,
|
|
98
97
|
};
|
|
99
|
-
pub use peers::allow_peer_talk;
|
|
100
98
|
pub use persist::{
|
|
101
99
|
persist_resolved_send, DeliveryBlocker, InitialDisposition, InternalSendKind,
|
|
102
100
|
LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
|
|
@@ -45,64 +45,6 @@ impl Default for ResultRoute {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
pub fn results_for_case(
|
|
49
|
-
workspace: &Path,
|
|
50
|
-
case_id: &str,
|
|
51
|
-
owner_team_id: Option<&str>,
|
|
52
|
-
requested_owner_team_id: Option<&str>,
|
|
53
|
-
) -> Result<Vec<serde_json::Value>, MessagingError> {
|
|
54
|
-
let current_db = workspace.join(".team/runtime/team.db");
|
|
55
|
-
let legacy_db = workspace.join(".team/team.db");
|
|
56
|
-
if current_db.exists() {
|
|
57
|
-
return results_for_case_in_db(
|
|
58
|
-
¤t_db,
|
|
59
|
-
case_id,
|
|
60
|
-
owner_team_id,
|
|
61
|
-
requested_owner_team_id,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
if legacy_db.exists() {
|
|
65
|
-
return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
|
|
66
|
-
}
|
|
67
|
-
Ok(Vec::new())
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
fn results_for_case_in_db(
|
|
71
|
-
db_path: &Path,
|
|
72
|
-
case_id: &str,
|
|
73
|
-
owner_team_id: Option<&str>,
|
|
74
|
-
requested_owner_team_id: Option<&str>,
|
|
75
|
-
) -> Result<Vec<serde_json::Value>, MessagingError> {
|
|
76
|
-
let conn = crate::db::schema::open_db(db_path)?;
|
|
77
|
-
// The second predicate works around
|
|
78
|
-
// `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
|
|
79
|
-
// it is not design intent.
|
|
80
|
-
let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
|
|
81
|
-
let sql = match owner_team_id {
|
|
82
|
-
Some(_) => format!(
|
|
83
|
-
"select envelope from results where ({case_predicate}) \
|
|
84
|
-
and (owner_team_id = ?2 or owner_team_id = ?3) \
|
|
85
|
-
order by created_at, result_id"
|
|
86
|
-
),
|
|
87
|
-
None => format!(
|
|
88
|
-
"select envelope from results where {case_predicate} order by created_at, result_id"
|
|
89
|
-
),
|
|
90
|
-
};
|
|
91
|
-
let mut stmt = conn.prepare(&sql)?;
|
|
92
|
-
let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
|
|
93
|
-
let rows = match (owner_team_id, requested_owner_team_id) {
|
|
94
|
-
(Some(team), Some(requested_team)) => {
|
|
95
|
-
stmt.query_map(params![case_id, team, requested_team], row_mapper)
|
|
96
|
-
}
|
|
97
|
-
(Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
|
|
98
|
-
(None, _) => stmt.query_map(params![case_id], row_mapper),
|
|
99
|
-
}?
|
|
100
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
101
|
-
rows.into_iter()
|
|
102
|
-
.map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
|
|
103
|
-
.collect()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
48
|
/// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
|
|
107
49
|
/// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
|
|
108
50
|
pub fn collect(
|