@team-agent/installer 0.2.11 → 0.3.1
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 +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1204 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1207 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +557 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1084 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +489 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +710 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +468 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +553 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +578 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +659 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +118 -112
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -503
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -91
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1243
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -144
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -693
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- package/src/team_agent/watch/__init__.py +0 -145
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
//! step 4 · event_log — `events.jsonl` 唯一审计流(真相源 `events.py`)。
|
|
2
|
+
//!
|
|
3
|
+
//! 每行一个事件:`{"ts": iso8601_utc, "event": type, ...fields}`,经
|
|
4
|
+
//! `json.dumps(sort_keys=True, ensure_ascii=False)` 序列化。**字节对拍要点(对抗检查)**:
|
|
5
|
+
//! - Python `json.dumps` 默认分隔符是 `", "` / `": "`(**带空格**)→ 自定义 [`PythonFormatter`]
|
|
6
|
+
//! (serde_json 默认无空格)。
|
|
7
|
+
//! - `sort_keys=True` 是**递归**排序 → [`sort_value`](preserve_order 下按 sorted 序重建;
|
|
8
|
+
//! envelope/state 仍需插入序,故不全局改 serde_json)。
|
|
9
|
+
//! - `ensure_ascii=False` → serde_json 默认即不转义非 ASCII(UTF-8 字面)。
|
|
10
|
+
//! - 轮转:5 MiB × 保留 5 archives(`events.jsonl.1..5`)。
|
|
11
|
+
//!
|
|
12
|
+
//! §10:无 unwrap/expect/panic;rotation 的 rename 失败best-effort 忽略(对应 Python
|
|
13
|
+
//! `except OSError: pass`,bug-084 的 os.replace 崩溃教训)。
|
|
14
|
+
//!
|
|
15
|
+
//! **有意分歧/已知边界(对抗检查确认,事件流里不会触发或不复刻 Python bug)**:
|
|
16
|
+
//! - `tail` 用 `\n` 行分割(JSONL 正解),**不复刻** Python `splitlines()` 在 U+2028/U+2029/
|
|
17
|
+
//! NEL 上误切 JSON 行的潜在 bug(§11 不重蹈)。
|
|
18
|
+
//! - 事件字段**可含 float**(如 `waited_sec: 0.0`,见 bug_082 golden):正常小数 serde_json 与
|
|
19
|
+
//! Python 字节一致(`0.0`/`0.5`…),由真 fixture round-trip 锁死。**已知边界**:`|v|<1e-4` 的
|
|
20
|
+
//! 指数浮点(`1e-07` vs serde `1e-7`、`1e-05` vs `0.00001`)与 NaN/Infinity、超 i64 整数会漂移 ——
|
|
21
|
+
//! 实测事件流不出现这些值,故未启 serde_json `arbitrary_precision`(全局影响 Number/preserve_order)
|
|
22
|
+
//! 或自定义 CPython float_repr;若将来引入,需重审。
|
|
23
|
+
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
24
|
+
|
|
25
|
+
use std::io::Write as _;
|
|
26
|
+
use std::path::{Path, PathBuf};
|
|
27
|
+
|
|
28
|
+
use serde::Serialize as _;
|
|
29
|
+
use serde_json::Value;
|
|
30
|
+
use thiserror::Error;
|
|
31
|
+
|
|
32
|
+
use crate::model::paths::logs_dir;
|
|
33
|
+
|
|
34
|
+
/// `events.py:17`:5 MiB。
|
|
35
|
+
pub const EVENT_LOG_ROTATE_BYTES: u64 = 5 * 1024 * 1024;
|
|
36
|
+
/// `events.py:18`:保留 5 个 archive。
|
|
37
|
+
pub const EVENT_LOG_ARCHIVE_KEEP: u32 = 5;
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Error)]
|
|
40
|
+
pub enum EventLogError {
|
|
41
|
+
#[error("io: {0}")]
|
|
42
|
+
Io(#[from] std::io::Error),
|
|
43
|
+
#[error("json: {0}")]
|
|
44
|
+
Json(#[from] serde_json::Error),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// serde_json `Formatter`,复刻 Python `json.dumps` 默认分隔符 `", "` / `": "`。
|
|
48
|
+
struct PythonFormatter;
|
|
49
|
+
|
|
50
|
+
impl serde_json::ser::Formatter for PythonFormatter {
|
|
51
|
+
fn begin_array_value<W: ?Sized + std::io::Write>(&mut self, w: &mut W, first: bool) -> std::io::Result<()> {
|
|
52
|
+
if first {
|
|
53
|
+
Ok(())
|
|
54
|
+
} else {
|
|
55
|
+
w.write_all(b", ")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
fn begin_object_key<W: ?Sized + std::io::Write>(&mut self, w: &mut W, first: bool) -> std::io::Result<()> {
|
|
59
|
+
if first {
|
|
60
|
+
Ok(())
|
|
61
|
+
} else {
|
|
62
|
+
w.write_all(b", ")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
fn begin_object_value<W: ?Sized + std::io::Write>(&mut self, w: &mut W) -> std::io::Result<()> {
|
|
66
|
+
w.write_all(b": ")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// 递归把 object 键排序(`sort_keys=True`)。preserve_order 下 `serde_json::Map` 是 IndexMap,
|
|
71
|
+
/// 按 sorted 序插入 → 序列化按插入序 = sorted。
|
|
72
|
+
fn sort_value(v: &Value) -> Value {
|
|
73
|
+
match v {
|
|
74
|
+
Value::Object(m) => {
|
|
75
|
+
let mut keys: Vec<&String> = m.keys().collect();
|
|
76
|
+
keys.sort_unstable();
|
|
77
|
+
let mut out = serde_json::Map::new();
|
|
78
|
+
for k in keys {
|
|
79
|
+
if let Some(val) = m.get(k) {
|
|
80
|
+
out.insert(k.clone(), sort_value(val));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
Value::Object(out)
|
|
84
|
+
}
|
|
85
|
+
Value::Array(a) => Value::Array(a.iter().map(sort_value).collect()),
|
|
86
|
+
other => other.clone(),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// `json.dumps(value, sort_keys=True, ensure_ascii=False)` 的字节等价(传入前先 [`sort_value`])。
|
|
91
|
+
fn to_python_json(value: &Value) -> String {
|
|
92
|
+
let mut buf = Vec::new();
|
|
93
|
+
let mut ser = serde_json::Serializer::with_formatter(&mut buf, PythonFormatter);
|
|
94
|
+
if value.serialize(&mut ser).is_err() {
|
|
95
|
+
return String::new();
|
|
96
|
+
}
|
|
97
|
+
String::from_utf8(buf).unwrap_or_default() // serde_json 必产合法 UTF-8
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// `datetime.now(utc).isoformat()` 字节等价:micros==0 省略小数秒(Python isoformat),否则 6 位微秒。
|
|
101
|
+
fn format_ts(dt: chrono::DateTime<chrono::Utc>) -> String {
|
|
102
|
+
if dt.timestamp_subsec_micros() == 0 {
|
|
103
|
+
dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
|
|
104
|
+
} else {
|
|
105
|
+
dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, false)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// `events.py:EventLog`。
|
|
110
|
+
pub struct EventLog {
|
|
111
|
+
path: PathBuf,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl EventLog {
|
|
115
|
+
/// `EventLog(workspace)`:路径 = `<workspace>/.team/logs/events.jsonl`。
|
|
116
|
+
pub fn new(workspace: &Path) -> Self {
|
|
117
|
+
Self { path: logs_dir(workspace).join("events.jsonl") }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// 直接指定 events.jsonl 路径(测试 / 非标准布局)。
|
|
121
|
+
pub fn at(path: PathBuf) -> Self {
|
|
122
|
+
Self { path }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// `EventLog.write`:追加一行 `{ts, event, **fields}`(sort_keys + Python 分隔符)。
|
|
126
|
+
/// 返回合并后的事件 Value。`fields` 应是 object;非 object 视作无字段。
|
|
127
|
+
pub fn write(&self, event_type: &str, fields: Value) -> Result<Value, EventLogError> {
|
|
128
|
+
if let Some(parent) = self.path.parent() {
|
|
129
|
+
std::fs::create_dir_all(parent)?;
|
|
130
|
+
}
|
|
131
|
+
let ts = format_ts(chrono::Utc::now());
|
|
132
|
+
let mut obj = serde_json::Map::new();
|
|
133
|
+
obj.insert("ts".to_string(), Value::String(ts));
|
|
134
|
+
obj.insert("event".to_string(), Value::String(event_type.to_string()));
|
|
135
|
+
if let Value::Object(f) = fields {
|
|
136
|
+
for (k, v) in f {
|
|
137
|
+
obj.insert(k, v);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
let event = Value::Object(obj);
|
|
141
|
+
self.maybe_rotate()?;
|
|
142
|
+
// 单次 write_all(line+"\n"):POSIX O_APPEND 对 <PIPE_BUF 写原子,避免并发写者交错(对抗 P1)。
|
|
143
|
+
let mut bytes = to_python_json(&sort_value(&event)).into_bytes();
|
|
144
|
+
bytes.push(b'\n');
|
|
145
|
+
let mut file = std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
|
|
146
|
+
file.write_all(&bytes)?;
|
|
147
|
+
Ok(event)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// `EventLog.tail`:末尾 `limit` 行,逐行 JSON parse;失败行 → `{"raw": line}`。
|
|
151
|
+
pub fn tail(&self, limit: usize) -> Result<Vec<Value>, EventLogError> {
|
|
152
|
+
if !self.path.exists() {
|
|
153
|
+
return Ok(Vec::new());
|
|
154
|
+
}
|
|
155
|
+
let text = std::fs::read_to_string(&self.path)?;
|
|
156
|
+
let lines: Vec<&str> = text.lines().collect();
|
|
157
|
+
// Python lines[-limit:]:limit==0 → lines[0:] = 全部(负零切片怪癖);limit>len → 全部。
|
|
158
|
+
let start = if limit == 0 { 0 } else { lines.len().saturating_sub(limit) };
|
|
159
|
+
let mut out = Vec::new();
|
|
160
|
+
for line in &lines[start..] {
|
|
161
|
+
match serde_json::from_str::<Value>(line) {
|
|
162
|
+
Ok(v) => out.push(v),
|
|
163
|
+
Err(_) => {
|
|
164
|
+
let mut m = serde_json::Map::new();
|
|
165
|
+
m.insert("raw".to_string(), Value::String((*line).to_string()));
|
|
166
|
+
out.push(Value::Object(m));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
Ok(out)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// `events.py:_maybe_rotate`:size >= 5 MiB → 丢最旧 + 移位 + current→.1。rename 失败忽略。
|
|
174
|
+
fn maybe_rotate(&self) -> Result<(), EventLogError> {
|
|
175
|
+
let size = match std::fs::metadata(&self.path) {
|
|
176
|
+
Ok(m) => m.len(),
|
|
177
|
+
Err(_) => return Ok(()), // 文件不存在 → 不轮转(对应 Python FileNotFoundError)
|
|
178
|
+
};
|
|
179
|
+
if size < EVENT_LOG_ROTATE_BYTES {
|
|
180
|
+
return Ok(());
|
|
181
|
+
}
|
|
182
|
+
let oldest = self.archive_path(EVENT_LOG_ARCHIVE_KEEP);
|
|
183
|
+
if oldest.exists() {
|
|
184
|
+
let _ = std::fs::remove_file(&oldest);
|
|
185
|
+
}
|
|
186
|
+
for idx in (1..EVENT_LOG_ARCHIVE_KEEP).rev() {
|
|
187
|
+
let src = self.archive_path(idx);
|
|
188
|
+
if src.exists() {
|
|
189
|
+
let _ = std::fs::rename(&src, self.archive_path(idx + 1));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let _ = std::fs::rename(&self.path, self.archive_path(1));
|
|
193
|
+
Ok(())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn archive_path(&self, index: u32) -> PathBuf {
|
|
197
|
+
let name = self.path.file_name().map_or_else(String::new, |n| n.to_string_lossy().into_owned());
|
|
198
|
+
self.path.with_file_name(format!("{name}.{index}"))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[cfg(test)]
|
|
203
|
+
mod tests {
|
|
204
|
+
#![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
|
|
205
|
+
use super::*;
|
|
206
|
+
use serde_json::json;
|
|
207
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
208
|
+
|
|
209
|
+
static SEQ: AtomicU32 = AtomicU32::new(0);
|
|
210
|
+
fn temp_ws() -> PathBuf {
|
|
211
|
+
let n = SEQ.fetch_add(1, Ordering::Relaxed);
|
|
212
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_ev_{}_{}", std::process::id(), n));
|
|
213
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
214
|
+
ws
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 确定性字节对拍:to_python_json(sort_value(..)) == Python json.dumps(sort_keys,ensure_ascii=False)。
|
|
218
|
+
#[test]
|
|
219
|
+
fn python_json_format_byte_parity() {
|
|
220
|
+
let v = json!({"event":"u","msg":"héllo🦀\n世界","nested":{"b":2,"a":[1,2]}});
|
|
221
|
+
assert_eq!(
|
|
222
|
+
to_python_json(&sort_value(&v)),
|
|
223
|
+
r#"{"event": "u", "msg": "héllo🦀\n世界", "nested": {"a": [1, 2], "b": 2}}"#
|
|
224
|
+
);
|
|
225
|
+
// 空 fields。
|
|
226
|
+
assert_eq!(to_python_json(&sort_value(&json!({"event":"empty"}))), r#"{"event": "empty"}"#);
|
|
227
|
+
// 类型:bool/null/int 与 Python 一致。
|
|
228
|
+
assert_eq!(
|
|
229
|
+
to_python_json(&sort_value(&json!({"missing":false,"x":null,"n":2}))),
|
|
230
|
+
r#"{"missing": false, "n": 2, "x": null}"#
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn write_sorts_keys_and_has_ts_event() {
|
|
236
|
+
let ws = temp_ws();
|
|
237
|
+
let log = EventLog::new(&ws);
|
|
238
|
+
log.write("schema.layout_rebuild", json!({"table":"messages","row_count_before":2,"row_count_after":2,"missing":false})).unwrap();
|
|
239
|
+
let line = std::fs::read_to_string(ws.join(".team/logs/events.jsonl")).unwrap();
|
|
240
|
+
let line = line.trim_end();
|
|
241
|
+
// 键全排序:event < missing < row_count_after < row_count_before < table < ts。
|
|
242
|
+
assert!(line.starts_with(r#"{"event": "schema.layout_rebuild", "missing": false, "row_count_after": 2, "row_count_before": 2, "table": "messages", "ts": "#));
|
|
243
|
+
// ts 是合法 rfc3339 UTC。
|
|
244
|
+
let v: Value = serde_json::from_str(line).unwrap();
|
|
245
|
+
let ts = v["ts"].as_str().unwrap();
|
|
246
|
+
assert!(chrono::DateTime::parse_from_rfc3339(ts).is_ok(), "ts 非合法 rfc3339: {ts}");
|
|
247
|
+
assert!(ts.ends_with("+00:00"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn tail_returns_last_n_and_raw_on_bad_line() {
|
|
252
|
+
let ws = temp_ws();
|
|
253
|
+
let log = EventLog::new(&ws);
|
|
254
|
+
for i in 0..5 {
|
|
255
|
+
log.write("e", json!({"i":i})).unwrap();
|
|
256
|
+
}
|
|
257
|
+
// 追加一行坏 JSON。
|
|
258
|
+
let p = ws.join(".team/logs/events.jsonl");
|
|
259
|
+
let mut f = std::fs::OpenOptions::new().append(true).open(&p).unwrap();
|
|
260
|
+
f.write_all(b"not json\n").unwrap();
|
|
261
|
+
drop(f);
|
|
262
|
+
let t = log.tail(2).unwrap();
|
|
263
|
+
assert_eq!(t.len(), 2);
|
|
264
|
+
assert_eq!(t[0]["i"], json!(4));
|
|
265
|
+
assert_eq!(t[1]["raw"], json!("not json"));
|
|
266
|
+
// 不存在 → 空。
|
|
267
|
+
assert!(EventLog::new(&temp_ws()).tail(10).unwrap().is_empty());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn rotation_at_threshold_shifts_archives_and_drops_oldest() {
|
|
272
|
+
let ws = temp_ws();
|
|
273
|
+
let p = logs_dir(&ws).join("events.jsonl");
|
|
274
|
+
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
275
|
+
let log = EventLog::at(p.clone());
|
|
276
|
+
// 预置 current >= 5MiB + 已有 .1..=.5 archive(各带标记内容)。
|
|
277
|
+
std::fs::write(&p, vec![b'x'; EVENT_LOG_ROTATE_BYTES as usize]).unwrap();
|
|
278
|
+
for i in 1..=EVENT_LOG_ARCHIVE_KEEP {
|
|
279
|
+
std::fs::write(p.with_file_name(format!("events.jsonl.{i}")), format!("arc{i}")).unwrap();
|
|
280
|
+
}
|
|
281
|
+
// 下一次 write 触发轮转:.5 丢弃,.4→.5 ... .1→.2,current→.1。
|
|
282
|
+
log.write("after.rotate", json!({})).unwrap();
|
|
283
|
+
// current 现在是轮转后的新文件,只含刚写的一条。
|
|
284
|
+
let new_current = std::fs::read_to_string(&p).unwrap();
|
|
285
|
+
assert_eq!(new_current.lines().count(), 1);
|
|
286
|
+
assert!(new_current.contains("after.rotate"));
|
|
287
|
+
// .1 = 旧 current(5MiB 的 x)。
|
|
288
|
+
assert_eq!(std::fs::metadata(p.with_file_name("events.jsonl.1")).unwrap().len(), EVENT_LOG_ROTATE_BYTES);
|
|
289
|
+
// .2 = 旧 .1(内容 "arc1");.5 = 旧 .4("arc4");旧 .5("arc5")被丢弃。
|
|
290
|
+
assert_eq!(std::fs::read_to_string(p.with_file_name("events.jsonl.2")).unwrap(), "arc1");
|
|
291
|
+
assert_eq!(std::fs::read_to_string(p.with_file_name("events.jsonl.5")).unwrap(), "arc4");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[test]
|
|
295
|
+
fn no_rotation_below_threshold() {
|
|
296
|
+
let ws = temp_ws();
|
|
297
|
+
let log = EventLog::new(&ws);
|
|
298
|
+
log.write("small", json!({})).unwrap();
|
|
299
|
+
log.write("small2", json!({})).unwrap();
|
|
300
|
+
// 未超阈值 → 无 .1 archive。
|
|
301
|
+
assert!(!ws.join(".team/logs/events.jsonl.1").exists());
|
|
302
|
+
assert_eq!(log.tail(10).unwrap().len(), 2);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 对抗 P4:ts micros==0 省略小数秒(Python isoformat);否则 6 位微秒。golden 自 Python。
|
|
306
|
+
#[test]
|
|
307
|
+
fn format_ts_matches_python_isoformat() {
|
|
308
|
+
let m0 = chrono::DateTime::from_timestamp(1_780_000_000, 0).unwrap();
|
|
309
|
+
assert_eq!(format_ts(m0), "2026-05-28T20:26:40+00:00");
|
|
310
|
+
let m = chrono::DateTime::from_timestamp(1_780_000_000, 123_456_000).unwrap();
|
|
311
|
+
assert_eq!(format_ts(m), "2026-05-28T20:26:40.123456+00:00");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 对抗 P7:tail(0) 复刻 Python lines[-0:] = 全部(负零切片怪癖)。
|
|
315
|
+
#[test]
|
|
316
|
+
fn tail_zero_returns_all_limit_clamps() {
|
|
317
|
+
let ws = temp_ws();
|
|
318
|
+
let log = EventLog::new(&ws);
|
|
319
|
+
for i in 0..3 {
|
|
320
|
+
log.write("e", json!({ "i": i })).unwrap();
|
|
321
|
+
}
|
|
322
|
+
assert_eq!(log.tail(0).unwrap().len(), 3, "tail(0) == 全部(Python [-0:])");
|
|
323
|
+
assert_eq!(log.tail(2).unwrap().len(), 2);
|
|
324
|
+
assert_eq!(log.tail(99).unwrap().len(), 3);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 对抗 P1:多写者并发 append 不交错(单次 write_all 原子)。每行须是完整合法 JSON。
|
|
328
|
+
#[test]
|
|
329
|
+
fn concurrent_appends_do_not_interleave() {
|
|
330
|
+
let ws = temp_ws();
|
|
331
|
+
let path = logs_dir(&ws).join("events.jsonl");
|
|
332
|
+
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
333
|
+
let threads: Vec<_> = (0..8)
|
|
334
|
+
.map(|t| {
|
|
335
|
+
let p = path.clone();
|
|
336
|
+
std::thread::spawn(move || {
|
|
337
|
+
let log = EventLog::at(p);
|
|
338
|
+
for i in 0..50 {
|
|
339
|
+
log.write("concurrent", json!({ "t": t, "i": i })).unwrap();
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
.collect();
|
|
344
|
+
for h in threads {
|
|
345
|
+
h.join().unwrap();
|
|
346
|
+
}
|
|
347
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
348
|
+
let lines: Vec<&str> = text.lines().collect();
|
|
349
|
+
assert_eq!(lines.len(), 8 * 50, "无半行/丢行");
|
|
350
|
+
for line in &lines {
|
|
351
|
+
// 每行完整合法 JSON 且含 event 键 → 未交错。
|
|
352
|
+
let v: Value = serde_json::from_str(line).unwrap_or_else(|e| panic!("交错坏行: {line:?} ({e})"));
|
|
353
|
+
assert_eq!(v["event"], json!("concurrent"));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 对抗 P5:真 events.jsonl golden(60 行 Python 序列化)逐行 round-trip 字节对拍。
|
|
358
|
+
#[test]
|
|
359
|
+
fn real_events_jsonl_fixture_round_trips_byte_identical() {
|
|
360
|
+
let fixture = include_str!(concat!(
|
|
361
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
362
|
+
"/../../snapshot/fixtures/bug_082_codex_trust/macmini_0210_own_events_after_corrected_send.jsonl"
|
|
363
|
+
));
|
|
364
|
+
let mut n = 0;
|
|
365
|
+
for line in fixture.lines() {
|
|
366
|
+
if line.is_empty() {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
let v: Value = serde_json::from_str(line).unwrap();
|
|
370
|
+
assert_eq!(to_python_json(&sort_value(&v)), line, "第 {n} 行 round-trip 不字节一致");
|
|
371
|
+
n += 1;
|
|
372
|
+
}
|
|
373
|
+
assert_eq!(n, 60, "fixture 应 60 行");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
//! `team-agent fake-worker` — Rust port of Python `team_agent.fake_worker` (SKELETON).
|
|
2
|
+
//!
|
|
3
|
+
//! Truth source (READ-ONLY) `team-agent-public` @ v0.2.11: `src/team_agent/fake_worker.py`.
|
|
4
|
+
//! A subscription-free backing program for `Provider::Fake`: it lets the real spawn path
|
|
5
|
+
//! (`launch(dry_run=false)` → tmux window) be exercised with NO real provider/subscription, so the
|
|
6
|
+
//! acceptance framework's cheap real-machine Tier-1 can drive the real daemon.
|
|
7
|
+
//!
|
|
8
|
+
//! Behavior (port target):
|
|
9
|
+
//! - print `TEAM_AGENT_FAKE_READY agent=<id>` (idle marker the daemon's classifier recognizes).
|
|
10
|
+
//! - read input line-by-line; on a non-empty line print `TEAM_AGENT_FAKE_WORKING agent=<id>`;
|
|
11
|
+
//! a `TEAM_AGENT_MESSAGE <json>` line OR a rendered `Team Agent message from …:` block (ending
|
|
12
|
+
//! `[team-agent-token:<tok>]`) → build a `result_envelope_v1` and report it via
|
|
13
|
+
//! `messaging::report_result`, echoing the envelope JSON; then print READY again.
|
|
14
|
+
//!
|
|
15
|
+
//! §84/MUST-NOT-13: a fake worker, no provider client. §10: implementation must be panic-free
|
|
16
|
+
//! (porter adds the deny + body; this skeleton is `unimplemented!()`).
|
|
17
|
+
#![allow(dead_code)]
|
|
18
|
+
#![cfg_attr(
|
|
19
|
+
not(test),
|
|
20
|
+
deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)
|
|
21
|
+
)]
|
|
22
|
+
|
|
23
|
+
use std::io::{BufRead, Write};
|
|
24
|
+
use std::path::Path;
|
|
25
|
+
|
|
26
|
+
use thiserror::Error;
|
|
27
|
+
|
|
28
|
+
#[derive(Debug, Error)]
|
|
29
|
+
pub enum FakeWorkerError {
|
|
30
|
+
#[error("io: {0}")]
|
|
31
|
+
Io(#[from] std::io::Error),
|
|
32
|
+
#[error("json: {0}")]
|
|
33
|
+
Json(#[from] serde_json::Error),
|
|
34
|
+
#[error("report_result: {0}")]
|
|
35
|
+
Report(String),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Run the fake-worker loop against `input`/`output` (stdin/stdout in `main`, in-memory in tests).
|
|
39
|
+
/// Port of `fake_worker.py:main` — the argv parsing (`--workspace`/`--agent-id`) lives in the
|
|
40
|
+
/// `fake-worker` subcommand dispatch; this entry takes them already resolved so it is unit-testable.
|
|
41
|
+
pub fn run(
|
|
42
|
+
workspace: &Path,
|
|
43
|
+
agent_id: &str,
|
|
44
|
+
input: impl BufRead,
|
|
45
|
+
mut output: impl Write,
|
|
46
|
+
) -> Result<(), FakeWorkerError> {
|
|
47
|
+
writeln!(output, "TEAM_AGENT_FAKE_READY agent={agent_id}")?;
|
|
48
|
+
let mut block: Option<RenderedBlock> = None;
|
|
49
|
+
for line in input.lines() {
|
|
50
|
+
let line = line?;
|
|
51
|
+
let trimmed = line.trim();
|
|
52
|
+
if trimmed.is_empty() {
|
|
53
|
+
if let Some(block) = block.as_mut() {
|
|
54
|
+
block.lines.push(String::new());
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
writeln!(output, "TEAM_AGENT_FAKE_WORKING agent={agent_id}")?;
|
|
60
|
+
if let Some(raw) = trimmed.strip_prefix("TEAM_AGENT_MESSAGE ") {
|
|
61
|
+
let payload: serde_json::Value = serde_json::from_str(raw)?;
|
|
62
|
+
let message_id = payload
|
|
63
|
+
.get("message_id")
|
|
64
|
+
.and_then(serde_json::Value::as_str)
|
|
65
|
+
.unwrap_or("unknown");
|
|
66
|
+
let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
|
|
67
|
+
report_fake_result(workspace, agent_id, message_id, task_id, &mut output)?;
|
|
68
|
+
block = None;
|
|
69
|
+
} else if let Some(parsed) = parse_rendered_header(trimmed) {
|
|
70
|
+
block = Some(parsed);
|
|
71
|
+
} else if let Some(token) = parse_token(trimmed) {
|
|
72
|
+
if let Some(current) = block.take() {
|
|
73
|
+
let _content = current.content();
|
|
74
|
+
report_fake_result(
|
|
75
|
+
workspace,
|
|
76
|
+
agent_id,
|
|
77
|
+
&token,
|
|
78
|
+
current.task_id.as_deref(),
|
|
79
|
+
&mut output,
|
|
80
|
+
)?;
|
|
81
|
+
}
|
|
82
|
+
} else if let Some(current) = block.as_mut() {
|
|
83
|
+
current.lines.push(trimmed.to_string());
|
|
84
|
+
}
|
|
85
|
+
writeln!(output, "TEAM_AGENT_FAKE_READY agent={agent_id}")?;
|
|
86
|
+
}
|
|
87
|
+
Ok(())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[derive(Debug)]
|
|
91
|
+
struct RenderedBlock {
|
|
92
|
+
task_id: Option<String>,
|
|
93
|
+
lines: Vec<String>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl RenderedBlock {
|
|
97
|
+
fn content(&self) -> String {
|
|
98
|
+
self.lines.join("\n").trim().to_string()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn parse_rendered_header(line: &str) -> Option<RenderedBlock> {
|
|
103
|
+
let rest = line.strip_prefix("Team Agent message from ")?;
|
|
104
|
+
let header = rest.strip_suffix(':')?;
|
|
105
|
+
let task_id = header
|
|
106
|
+
.split_once(" for ")
|
|
107
|
+
.map(|(_, task)| task.trim().to_string())
|
|
108
|
+
.filter(|task| !task.is_empty());
|
|
109
|
+
Some(RenderedBlock { task_id, lines: Vec::new() })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn parse_token(line: &str) -> Option<String> {
|
|
113
|
+
let token = line
|
|
114
|
+
.strip_prefix("[team-agent-token:")
|
|
115
|
+
.and_then(|rest| rest.strip_suffix(']'))?
|
|
116
|
+
.trim();
|
|
117
|
+
if token.is_empty() {
|
|
118
|
+
None
|
|
119
|
+
} else {
|
|
120
|
+
Some(token.to_string())
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn report_fake_result(
|
|
125
|
+
workspace: &Path,
|
|
126
|
+
agent_id: &str,
|
|
127
|
+
message_id: &str,
|
|
128
|
+
task_id: Option<&str>,
|
|
129
|
+
output: &mut impl Write,
|
|
130
|
+
) -> Result<(), FakeWorkerError> {
|
|
131
|
+
let envelope = fake_envelope(workspace, agent_id, message_id, task_id);
|
|
132
|
+
crate::messaging::report_result(workspace, &envelope)
|
|
133
|
+
.map_err(|e| FakeWorkerError::Report(e.to_string()))?;
|
|
134
|
+
writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
|
|
135
|
+
Ok(())
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn fake_envelope(
|
|
139
|
+
workspace: &Path,
|
|
140
|
+
agent_id: &str,
|
|
141
|
+
message_id: &str,
|
|
142
|
+
task_id: Option<&str>,
|
|
143
|
+
) -> serde_json::Value {
|
|
144
|
+
serde_json::json!({
|
|
145
|
+
"schema_version": "result_envelope_v1",
|
|
146
|
+
"task_id": task_id.unwrap_or("manual"),
|
|
147
|
+
"agent_id": agent_id,
|
|
148
|
+
"status": "success",
|
|
149
|
+
"summary": format!("Fake worker handled message {message_id}"),
|
|
150
|
+
"changes": [],
|
|
151
|
+
"tests": [{"command": "fake-provider", "status": "passed"}],
|
|
152
|
+
"risks": [],
|
|
153
|
+
"artifacts": [{
|
|
154
|
+
"path": workspace.join(".team").join("logs").join(format!("{agent_id}.scrollback")).to_string_lossy(),
|
|
155
|
+
"description": "tmux scrollback for fake worker"
|
|
156
|
+
}],
|
|
157
|
+
"next_actions": []
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[cfg(test)]
|
|
162
|
+
mod tests {
|
|
163
|
+
//! FAKE-WORKER RED — `run` is the `unimplemented!()` skeleton today, so these PANIC (RED) until the
|
|
164
|
+
//! porter ports `fake_worker.py`. Golden captured live from
|
|
165
|
+
//! `PYTHONPATH=…/team-agent-public/src python3 -m team_agent.fake_worker --workspace <ws> --agent-id w1`:
|
|
166
|
+
//! both the `TEAM_AGENT_MESSAGE {json}` line and the rendered `Team Agent message from leader for t1:`
|
|
167
|
+
//! block (ending `[team-agent-token:m1]`) report the SAME `result_envelope_v1` via the real
|
|
168
|
+
//! `messaging::report_result`, and the daemon-recognizable READY/WORKING markers are printed.
|
|
169
|
+
use super::run;
|
|
170
|
+
use std::io::Cursor;
|
|
171
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
172
|
+
|
|
173
|
+
use crate::db::schema::open_db;
|
|
174
|
+
use crate::message_store::MessageStore;
|
|
175
|
+
|
|
176
|
+
fn fake_ws() -> std::path::PathBuf {
|
|
177
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
178
|
+
let dir = std::env::temp_dir().join(format!(
|
|
179
|
+
"ta-rs-fakew-{}-{}",
|
|
180
|
+
std::process::id(),
|
|
181
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
182
|
+
));
|
|
183
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
184
|
+
dir
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Read back the single result the fake worker stored via `messaging::report_result`.
|
|
188
|
+
fn stored_result(ws: &std::path::Path) -> (String, String, String, serde_json::Value) {
|
|
189
|
+
let store = MessageStore::open(ws).unwrap();
|
|
190
|
+
let conn = open_db(store.db_path()).unwrap();
|
|
191
|
+
let mut stmt = conn.prepare("select task_id, agent_id, status, envelope from results").unwrap();
|
|
192
|
+
let row = stmt
|
|
193
|
+
.query_row([], |r| {
|
|
194
|
+
Ok((
|
|
195
|
+
r.get::<_, String>(0)?,
|
|
196
|
+
r.get::<_, String>(1)?,
|
|
197
|
+
r.get::<_, String>(2)?,
|
|
198
|
+
r.get::<_, String>(3)?,
|
|
199
|
+
))
|
|
200
|
+
})
|
|
201
|
+
.expect("exactly one stored fake-worker result");
|
|
202
|
+
let envelope: serde_json::Value = serde_json::from_str(&row.3).unwrap();
|
|
203
|
+
(row.0, row.1, row.2, envelope)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// Assert the stored envelope matches the captured Python golden (message_id m1, task_id t1).
|
|
207
|
+
fn assert_golden_envelope(ws: &std::path::Path) {
|
|
208
|
+
let (task_id, agent_id, status, env) = stored_result(ws);
|
|
209
|
+
assert_eq!(
|
|
210
|
+
(task_id.as_str(), agent_id.as_str(), status.as_str()),
|
|
211
|
+
("t1", "w1", "success"),
|
|
212
|
+
"stored result row must be task_id=t1 agent_id=w1 status=success"
|
|
213
|
+
);
|
|
214
|
+
assert_eq!(env["schema_version"], serde_json::json!("result_envelope_v1"));
|
|
215
|
+
assert_eq!(env["summary"], serde_json::json!("Fake worker handled message m1"));
|
|
216
|
+
assert_eq!(env["tests"], serde_json::json!([{"command": "fake-provider", "status": "passed"}]));
|
|
217
|
+
let artifact_path = env["artifacts"][0]["path"].as_str().unwrap_or_default();
|
|
218
|
+
assert!(
|
|
219
|
+
artifact_path.ends_with(".team/logs/w1.scrollback"),
|
|
220
|
+
"artifact path must be <ws>/.team/logs/w1.scrollback; got {artifact_path}"
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// TEAM_AGENT_MESSAGE form: `TEAM_AGENT_MESSAGE {json}` line → real report_result stores the golden
|
|
225
|
+
// result_envelope_v1; READY (idle) + WORKING markers printed. Golden _report_fake_result.
|
|
226
|
+
#[test]
|
|
227
|
+
fn fake_worker_team_agent_message_reports_golden_envelope() {
|
|
228
|
+
let ws = fake_ws();
|
|
229
|
+
let input = "TEAM_AGENT_MESSAGE {\"message_id\":\"m1\",\"task_id\":\"t1\"}\n";
|
|
230
|
+
let mut out: Vec<u8> = Vec::new();
|
|
231
|
+
run(&ws, "w1", Cursor::new(input.as_bytes()), &mut out).expect("fake worker run");
|
|
232
|
+
|
|
233
|
+
assert_golden_envelope(&ws);
|
|
234
|
+
let printed = String::from_utf8(out).unwrap();
|
|
235
|
+
assert!(printed.contains("TEAM_AGENT_FAKE_READY agent=w1"), "must print the READY idle marker; got {printed}");
|
|
236
|
+
assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "must print the WORKING marker on a non-empty line; got {printed}");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Rendered-message form: a `Team Agent message from leader for t1:` block ending `[team-agent-token:m1]`
|
|
240
|
+
// parses to the SAME envelope (message_id m1 from the token, task_id t1 from `for t1`). Golden
|
|
241
|
+
// _parse_rendered_message + _report_fake_result.
|
|
242
|
+
#[test]
|
|
243
|
+
fn fake_worker_rendered_message_reports_same_golden_envelope() {
|
|
244
|
+
let ws = fake_ws();
|
|
245
|
+
let input = "Team Agent message from leader for t1:\nplease do X\n[team-agent-token:m1]\n";
|
|
246
|
+
let mut out: Vec<u8> = Vec::new();
|
|
247
|
+
run(&ws, "w1", Cursor::new(input.as_bytes()), &mut out).expect("fake worker run");
|
|
248
|
+
|
|
249
|
+
assert_golden_envelope(&ws);
|
|
250
|
+
let printed = String::from_utf8(out).unwrap();
|
|
251
|
+
assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "rendered form prints WORKING per line; got {printed}");
|
|
252
|
+
}
|
|
253
|
+
}
|