@team-agent/installer 0.2.10 โ 0.3.0
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 +1077 -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 +1141 -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 +436 -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 +1063 -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 +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -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 +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -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 +487 -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 +1833 -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 +933 -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 +685 -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 +159 -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 +388 -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 +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -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 +537 -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 +582 -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 +656 -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 +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -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 +90 -106
- 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 -83
- package/src/team_agent/coordinator/lifecycle.py +0 -363
- 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 -200
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -111
- 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 -254
- package/src/team_agent/messaging/delivery.py +0 -473
- 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 -457
- 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 -86
- 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 -1239
- 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 -143
- 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 -602
- 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,829 @@
|
|
|
1
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
2
|
+
#![allow(non_snake_case)]
|
|
3
|
+
use super::*;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
|
|
6
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
7
|
+
// ยง19 ๆฃๅญ็ฌฆไธฒ โ enum:SkillTargetโProvider ๅ
ณ่ (skeleton:112 codexโCodex,
|
|
8
|
+
// claudeโClaudeCode;allโNone)ใPython _skill_dest_dir ๆฎ target ้ dirใ
|
|
9
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
10
|
+
|
|
11
|
+
#[test]
|
|
12
|
+
fn skill_target_codex_maps_to_provider_codex() {
|
|
13
|
+
// skeleton line 112: codexโCodex.
|
|
14
|
+
assert_eq!(SkillTarget::Codex.provider(), Some(Provider::Codex));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn skill_target_claude_maps_to_provider_claude_code() {
|
|
19
|
+
// skeleton line 112: claudeโClaudeCode (NOT bare Claude โ ยง3 claude vs claude_code ไธ่ฝๆผๅฝไธ).
|
|
20
|
+
assert_eq!(SkillTarget::Claude.provider(), Some(Provider::ClaudeCode));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[test]
|
|
24
|
+
fn skill_target_all_has_no_single_provider() {
|
|
25
|
+
// `All` fan-out ไธค่
โ ๆ ๅไธ provider.
|
|
26
|
+
assert_eq!(SkillTarget::All.provider(), None);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
30
|
+
// _skill_dest_dir (commands.py:467-472):claudeโ~/.claude/skills/team-agent,
|
|
31
|
+
// ๅ
ถไฝ(ๅซ codex)โ~/.codex/skills/team-agent;AllโNone(้ๅ dir,fan-out)ใ
|
|
32
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn dest_dir_codex_resolves_to_dot_codex() {
|
|
36
|
+
// commands.py:471 โ codex (else branch) โ ~/.codex/skills/team-agent.
|
|
37
|
+
let home = Path::new("/home/testuser");
|
|
38
|
+
let got = SkillTarget::Codex.dest_dir(home);
|
|
39
|
+
assert_eq!(
|
|
40
|
+
got,
|
|
41
|
+
Some(SkillDestDir(PathBuf::from(
|
|
42
|
+
"/home/testuser/.codex/skills/team-agent"
|
|
43
|
+
)))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#[test]
|
|
48
|
+
fn dest_dir_claude_resolves_to_dot_claude() {
|
|
49
|
+
// commands.py:469 โ claude โ ~/.claude/skills/team-agent.
|
|
50
|
+
let home = Path::new("/home/testuser");
|
|
51
|
+
let got = SkillTarget::Claude.dest_dir(home);
|
|
52
|
+
assert_eq!(
|
|
53
|
+
got,
|
|
54
|
+
Some(SkillDestDir(PathBuf::from(
|
|
55
|
+
"/home/testuser/.claude/skills/team-agent"
|
|
56
|
+
)))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn dest_dir_all_is_none_not_single_dir() {
|
|
62
|
+
// `All` ๅบ fan-out ๅฐไธค่
โ ้ๅ dir โ None (skeleton:116).
|
|
63
|
+
let home = Path::new("/home/testuser");
|
|
64
|
+
assert_eq!(SkillTarget::All.dest_dir(home), None);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
68
|
+
// Version ๅไธ็็ธๆบ:env!("CARGO_PKG_VERSION") โ ไฟฎๅๆบๆผ็งป
|
|
69
|
+
// (pyproject 0.1.4 vs package.json 0.2.11)ใcurrent() == Cargo.toml ็ๆฌ,
|
|
70
|
+
// ็ฆๆๆ็ฌฌไบๅคใas_str() ๅณ้ไผ ใ
|
|
71
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
72
|
+
|
|
73
|
+
#[test]
|
|
74
|
+
fn version_current_equals_cargo_pkg_version() {
|
|
75
|
+
// ๅไธ็็ธๆบ โ ็ฆๆๆใcurrent() ๅฟ
้กป == ็ผ่ฏๆ CARGO_PKG_VERSION.
|
|
76
|
+
assert_eq!(Version::current().as_str(), env!("CARGO_PKG_VERSION"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn version_current_is_not_a_hand_copied_python_drift_literal() {
|
|
81
|
+
// STRENGTHENED (gate w59ds828k): the old test only asserted !is_empty() && != "dev",
|
|
82
|
+
// which a buggy impl returning a hardcoded "0.2.11" copied from package.json would PASS โ
|
|
83
|
+
// exactly the double-source-drift bug the subsystem forbids. Now CONCRETE & falsifiable.
|
|
84
|
+
//
|
|
85
|
+
// CONCRETE golden: workspace Cargo.toml version == "0.0.0" (Phase 0; team-agent-rs
|
|
86
|
+
// Cargo.toml:12). CARGO_PKG_VERSION therefore resolves to "0.0.0" โ which differs from
|
|
87
|
+
// BOTH Python drift sources (pyproject.toml 0.1.4 / package.json 0.2.11). So a porter who
|
|
88
|
+
// hand-copies either Python literal instead of using env!("CARGO_PKG_VERSION") FAILS here.
|
|
89
|
+
let v = Version::current();
|
|
90
|
+
assert_eq!(
|
|
91
|
+
v.as_str(),
|
|
92
|
+
env!("CARGO_PKG_VERSION"),
|
|
93
|
+
"single source of truth = CARGO_PKG_VERSION"
|
|
94
|
+
);
|
|
95
|
+
assert_ne!(v.as_str(), "0.1.4", "must not hand-copy pyproject.toml drift source");
|
|
96
|
+
assert_ne!(v.as_str(), "0.2.11", "must not hand-copy package.json drift source");
|
|
97
|
+
assert_ne!(v.as_str(), "dev", "must not be install.mjs:54 'dev' fallback");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn no_literal_version_string_hardcoded_in_packaging_code() {
|
|
102
|
+
// STRENGTHENED (gate w59ds828k): grep-assert the production CODE (comments stripped)
|
|
103
|
+
// contains no hand-copied semver literal โ only env!("CARGO_PKG_VERSION") may supply the
|
|
104
|
+
// version. Reads the file at test time via CARGO_MANIFEST_DIR. The Python drift literals
|
|
105
|
+
// 0.1.4 / 0.2.11 legitimately appear in doc/line comments documenting the bug, so we strip
|
|
106
|
+
// comment text first and scan only executable code. This is the one place where
|
|
107
|
+
// "double-source-drift forbidden" is statically checked against the source itself.
|
|
108
|
+
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/packaging/types.rs"))
|
|
109
|
+
.expect("read own source");
|
|
110
|
+
// Production region only (the #[cfg(test)] mod uses these literals as golden anti-examples):
|
|
111
|
+
let prod = match src.find("#[cfg(test)]") {
|
|
112
|
+
Some(i) => &src[..i],
|
|
113
|
+
None => &src[..],
|
|
114
|
+
};
|
|
115
|
+
// Strip everything from the first `//` on each line (covers `//!` doc + `//` line comments).
|
|
116
|
+
let code: String = prod
|
|
117
|
+
.lines()
|
|
118
|
+
.map(|line| match line.find("//") {
|
|
119
|
+
Some(i) => &line[..i],
|
|
120
|
+
None => line,
|
|
121
|
+
})
|
|
122
|
+
.collect::<Vec<_>>()
|
|
123
|
+
.join("\n");
|
|
124
|
+
assert!(
|
|
125
|
+
!code.contains("0.1.4"),
|
|
126
|
+
"packaging.rs code must not hand-copy pyproject 0.1.4 (use env!(CARGO_PKG_VERSION))"
|
|
127
|
+
);
|
|
128
|
+
assert!(
|
|
129
|
+
!code.contains("0.2.11"),
|
|
130
|
+
"packaging.rs code must not hand-copy package.json 0.2.11 (use env!(CARGO_PKG_VERSION))"
|
|
131
|
+
);
|
|
132
|
+
// The ONLY version source allowed is env!("CARGO_PKG_VERSION") โ assert it is the source.
|
|
133
|
+
assert!(
|
|
134
|
+
code.contains("CARGO_PKG_VERSION"),
|
|
135
|
+
"Version::current() must derive from env!(\"CARGO_PKG_VERSION\")"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn version_serde_transparent_roundtrip() {
|
|
141
|
+
// #[serde(transparent)] โ ๅบๅๅไธบ่ฃธๅญ็ฌฆไธฒ,้ {"0":"..."}.
|
|
142
|
+
let v = Version("1.2.3".to_string());
|
|
143
|
+
let json = serde_json::to_string(&v).unwrap();
|
|
144
|
+
assert_eq!(json, "\"1.2.3\"");
|
|
145
|
+
let back: Version = serde_json::from_str(&json).unwrap();
|
|
146
|
+
assert_eq!(back, v);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
150
|
+
// platform_support (ยง8 ๅฆๅฎๅฃฐๆ):macOS/Linux ๅ็;Windows ๅ็ไธ็ญ
|
|
151
|
+
// (WezTerm/ConPTY,่ง transport-backend-design)ใไธๅ่ฃ
ๅ
ผๅฎนใ
|
|
152
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
153
|
+
|
|
154
|
+
#[test]
|
|
155
|
+
fn macos_aarch64_is_native() {
|
|
156
|
+
assert_eq!(
|
|
157
|
+
platform_support(ReleaseTarget::MacosAarch64),
|
|
158
|
+
PlatformSupport::Native
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn macos_x8664_is_native() {
|
|
164
|
+
assert_eq!(
|
|
165
|
+
platform_support(ReleaseTarget::MacosX8664),
|
|
166
|
+
PlatformSupport::Native
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn linux_x8664_is_native() {
|
|
172
|
+
assert_eq!(
|
|
173
|
+
platform_support(ReleaseTarget::LinuxX8664),
|
|
174
|
+
PlatformSupport::Native
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn linux_aarch64_is_native() {
|
|
180
|
+
assert_eq!(
|
|
181
|
+
platform_support(ReleaseTarget::LinuxAarch64),
|
|
182
|
+
PlatformSupport::Native
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[test]
|
|
187
|
+
fn windows_x8664_is_native_per_transport_design() {
|
|
188
|
+
// skeleton:203/211 โ Windows ๅ็ไธ็ญ (WezTerm/ConPTY,้ tmux)ใ
|
|
189
|
+
// ไธๆฏ Unsupported,ไธๆฏ RequiresWslTmux โ ๆฏ Native.
|
|
190
|
+
assert_eq!(
|
|
191
|
+
platform_support(ReleaseTarget::WindowsX8664),
|
|
192
|
+
PlatformSupport::Native
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
197
|
+
// doctor โ typed DoctorStatus (commands.py:218-260)ใ
|
|
198
|
+
// error ่ทฏๅพ + ็ฒพ็กฎๆถๆฏ:
|
|
199
|
+
// - --fix ๆ --gate โ TeamAgentError("--fix requires --gate") (commands.py:221)
|
|
200
|
+
// - unknown doctor gate โ "unknown doctor gate: <g>" (commands.py:235)
|
|
201
|
+
// - schema layout drift โ HasBlockers{SchemaLayoutDrift} (commands.py:242-250)
|
|
202
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
203
|
+
|
|
204
|
+
fn doctor_opts(workspace: &Path) -> DoctorOptions {
|
|
205
|
+
DoctorOptions {
|
|
206
|
+
workspace: workspace.to_path_buf(),
|
|
207
|
+
gate: None,
|
|
208
|
+
fix: false,
|
|
209
|
+
cleanup_orphans: false,
|
|
210
|
+
confirm: false,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[test]
|
|
215
|
+
fn doctor_fix_without_gate_is_invalid_options() {
|
|
216
|
+
// commands.py:220-221 โ `--fix requires --gate`.
|
|
217
|
+
let ws = PathBuf::from("/tmp/ws-doctor-fix");
|
|
218
|
+
let mut opts = doctor_opts(&ws);
|
|
219
|
+
opts.fix = true;
|
|
220
|
+
opts.gate = None;
|
|
221
|
+
let err = doctor(&opts).expect_err("fix without gate must error");
|
|
222
|
+
match err {
|
|
223
|
+
PackagingError::InvalidOptions(msg) => {
|
|
224
|
+
assert!(
|
|
225
|
+
msg.contains("--fix requires --gate"),
|
|
226
|
+
"expected '--fix requires --gate', got: {msg}"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
other => panic!("expected InvalidOptions, got {other:?}"),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// TEST-SUPPORT seed helper (real impl โ pure test scaffolding, uses rusqlite directly):
|
|
234
|
+
/// build a workspace whose `.team/runtime/team.db` has the LEGACY drifted layout
|
|
235
|
+
/// (owner_team_id appended as the last column on the 4 managed tables, user_version=1).
|
|
236
|
+
/// This is the exact fixture migration.rs::build_legacy uses; schema_diagnosis on it yields
|
|
237
|
+
/// non-empty layout_diffs โ doctor() must surface SchemaLayoutDrift. Returns the workspace.
|
|
238
|
+
fn seed_workspace_with_drifted_db(tag: &str) -> PathBuf {
|
|
239
|
+
use rusqlite::Connection;
|
|
240
|
+
let ws = std::env::temp_dir().join(format!("ta-doctor-drift-{}-{}", std::process::id(), tag));
|
|
241
|
+
let db = ws.join(".team").join("runtime").join("team.db");
|
|
242
|
+
std::fs::create_dir_all(db.parent().unwrap()).expect("seed runtime dir");
|
|
243
|
+
let conn = Connection::open(&db).expect("open drifted db");
|
|
244
|
+
// Legacy layout: owner_team_id is the LAST column โ physical column-order drift vs canonical.
|
|
245
|
+
conn.execute_batch(
|
|
246
|
+
"create table messages (message_id text primary key, task_id text, sender text, recipient text, reply_to text, requires_ack integer, status text, content text, artifact_refs text, created_at text, updated_at text, delivered_at text, acknowledged_at text, error text, delivery_attempts integer not null default 0, owner_team_id text);
|
|
247
|
+
create table results (result_id text primary key, task_id text not null, agent_id text not null, envelope text not null, status text not null, created_at text not null, owner_team_id text);
|
|
248
|
+
create table scheduled_events (id integer primary key, due_at text not null, target text not null, kind text not null, payload_json text not null, status text not null, created_at text not null, fired_at text, result_json text, owner_team_id text);
|
|
249
|
+
create table agent_health (agent_id text not null, status text not null, last_output_at text, context_usage_pct integer, current_task_id text, updated_at text not null, owner_team_id text);
|
|
250
|
+
pragma user_version = 1;",
|
|
251
|
+
)
|
|
252
|
+
.expect("seed legacy schema");
|
|
253
|
+
drop(conn);
|
|
254
|
+
ws
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn doctor_on_clean_workspace_no_drift_is_ok() {
|
|
259
|
+
// ๆ schema layout drift (็ฉบ/ๆ db) โ DoctorStatus::Ok.
|
|
260
|
+
// schema_diagnosis(missing db) โ layout_diffs ็ฉบ โ ้ HasBlockers(SchemaLayoutDrift).
|
|
261
|
+
// NOTE (gate w59ds828k): cleanโOk cannot distinguish "gates ran & passed" from "gates not
|
|
262
|
+
// wired" because step 11/12 gate entities live elsewhere. The positive drift test below
|
|
263
|
+
// (doctor_drifted_db_emits_schema_layout_drift_blocker) is what proves doctor() actually
|
|
264
|
+
// READS schema_diagnosis and emits the typed SchemaLayoutDrift blocker on real drift.
|
|
265
|
+
let dir = std::env::temp_dir().join(format!("ta-doctor-clean-{}", std::process::id()));
|
|
266
|
+
let _ = std::fs::create_dir_all(&dir);
|
|
267
|
+
let opts = doctor_opts(&dir);
|
|
268
|
+
let status = doctor(&opts).expect("clean workspace doctor should succeed");
|
|
269
|
+
assert_eq!(status, DoctorStatus::Ok);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn doctor_drifted_db_emits_schema_layout_drift_blocker() {
|
|
274
|
+
// STRENGTHENED (gate w59ds828k): the ONLY drift assertion previously
|
|
275
|
+
// (doctor_status_has_blockers_carries_typed_source) was a pure serde test that hand-built
|
|
276
|
+
// the Blocker โ it never proved doctor() EMITS SchemaLayoutDrift from a real drifted db.
|
|
277
|
+
// This drives real doctor() on a SEEDED drifted team.db and pins the CONCRETE golden:
|
|
278
|
+
// commands.py:242-250 โ schema layout drift โ coordinator.schema_error ==
|
|
279
|
+
// "team.db physical layout drift detected" (EXACT string, commands.py:248).
|
|
280
|
+
let ws = seed_workspace_with_drifted_db("blocker");
|
|
281
|
+
let opts = doctor_opts(&ws);
|
|
282
|
+
let status = doctor(&opts).expect("doctor on drifted workspace should succeed (returns blockers)");
|
|
283
|
+
match status {
|
|
284
|
+
DoctorStatus::HasBlockers { blockers } => {
|
|
285
|
+
let drift = blockers
|
|
286
|
+
.iter()
|
|
287
|
+
.find(|b| b.source == BlockerSource::SchemaLayoutDrift)
|
|
288
|
+
.expect("must surface a SchemaLayoutDrift blocker");
|
|
289
|
+
// EXACT golden string from commands.py:248.
|
|
290
|
+
assert_eq!(
|
|
291
|
+
drift.detail, "team.db physical layout drift detected",
|
|
292
|
+
"schema_error golden must match commands.py:248 verbatim"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
DoctorStatus::Ok => panic!("drifted team.db must NOT report Ok โ layout_diffs non-empty"),
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[test]
|
|
300
|
+
fn doctor_status_ok_serializes_with_status_tag() {
|
|
301
|
+
// #[serde(tag = "status")] โ Ok โ {"status":"ok"}.
|
|
302
|
+
let json = serde_json::to_string(&DoctorStatus::Ok).unwrap();
|
|
303
|
+
assert_eq!(json, "{\"status\":\"ok\"}");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn doctor_status_has_blockers_carries_typed_source() {
|
|
308
|
+
// HasBlockers serde:tag status=has_blockers + blockers[].source snake_case.
|
|
309
|
+
let status = DoctorStatus::HasBlockers {
|
|
310
|
+
blockers: vec![Blocker {
|
|
311
|
+
source: BlockerSource::SchemaLayoutDrift,
|
|
312
|
+
detail: "team.db physical layout drift detected".to_string(),
|
|
313
|
+
}],
|
|
314
|
+
};
|
|
315
|
+
let json = serde_json::to_string(&status).unwrap();
|
|
316
|
+
assert!(json.contains("\"status\":\"has_blockers\""), "got: {json}");
|
|
317
|
+
assert!(json.contains("\"source\":\"schema_layout_drift\""), "got: {json}");
|
|
318
|
+
// detail ็ฒพ็กฎ == commands.py:248 schema_error ๆๆฌ.
|
|
319
|
+
assert!(
|
|
320
|
+
json.contains("team.db physical layout drift detected"),
|
|
321
|
+
"got: {json}"
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn blocker_source_serde_exact_snake_case_strings() {
|
|
327
|
+
// ยง19 ็ฉทๅฐฝ enum ๅบๅๅ == Python ๆฃๅญ็ฌฆไธฒ็ญไปท (้ variant ้).
|
|
328
|
+
let cases: &[(BlockerSource, &str)] = &[
|
|
329
|
+
(BlockerSource::SchemaLayoutDrift, "\"schema_layout_drift\""),
|
|
330
|
+
(BlockerSource::OrphanCoordinator, "\"orphan_coordinator\""),
|
|
331
|
+
(BlockerSource::CommsGate, "\"comms_gate\""),
|
|
332
|
+
(BlockerSource::PathNotConfigured, "\"path_not_configured\""),
|
|
333
|
+
];
|
|
334
|
+
for (src, want) in cases {
|
|
335
|
+
assert_eq!(&serde_json::to_string(src).unwrap(), want);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
340
|
+
// repair_schema โ ่ฝฌ่ฐ step 3 fix_schema_layout(workspace, SCHEMA_VERSION=3)ใ
|
|
341
|
+
// (commands.py:239-240 doctor --fix-schema)ใ
|
|
342
|
+
// ๅณ็ญ:
|
|
343
|
+
// - db ไธๅญๅจ โ Missing โ ๆ drift โ UpToDate (่ฝฌ่ฐๅคๅฃณ)ใ
|
|
344
|
+
// - ๆๆดป่ท้ โ Blocked{reason:"active_lock"} ไธไธๅๅคไปฝใ
|
|
345
|
+
// ็ ดๅๆง rebuild ็่ทฏๅพ #[ignore](้็ db fixture / ็ๆบ)ใ
|
|
346
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
347
|
+
|
|
348
|
+
#[test]
|
|
349
|
+
fn repair_schema_missing_db_is_up_to_date() {
|
|
350
|
+
// db ไธๅญๅจ (.team/runtime/team.db) โ fix_schema_layout ่ฟ Missing โ
|
|
351
|
+
// packaging ๅ
ๆ UpToDate (ๆ drift ้่ฟ็งป)ใ
|
|
352
|
+
let dir = std::env::temp_dir().join(format!("ta-repair-missing-{}", std::process::id()));
|
|
353
|
+
let _ = std::fs::create_dir_all(&dir);
|
|
354
|
+
let outcome = repair_schema(&dir).expect("missing db repair should succeed");
|
|
355
|
+
match outcome {
|
|
356
|
+
MigrationOutcome::UpToDate { diagnosis } => {
|
|
357
|
+
// schema_diagnosis(missing) โ ok=true, layout_diffs ็ฉบ.
|
|
358
|
+
assert!(diagnosis.layout_diffs.is_empty(), "missing db has no drift");
|
|
359
|
+
}
|
|
360
|
+
other => panic!("expected UpToDate for missing db, got {other:?}"),
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#[test]
|
|
365
|
+
#[ignore = "REAL-MACHINE-E2E: needs real drifted team.db fixture + destructive rebuild"]
|
|
366
|
+
fn repair_schema_drifted_db_migrates() {
|
|
367
|
+
// ็ drift fixture โ Migrated{fix: FixResult::Fixed{rebuilds non-empty}}.
|
|
368
|
+
let ws = PathBuf::from("/nonexistent-fixture");
|
|
369
|
+
let outcome = repair_schema(&ws).expect("drift repair");
|
|
370
|
+
assert!(matches!(outcome, MigrationOutcome::Migrated { .. }));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#[test]
|
|
374
|
+
#[ignore = "REAL-MACHINE-E2E: needs a held active lock on team.db (concurrent BEGIN IMMEDIATE)"]
|
|
375
|
+
fn repair_schema_active_lock_is_blocked_no_backup() {
|
|
376
|
+
// ๆๆดป่ท้ โ Blocked{reason:"active_lock"};ไธไธๅๅคไปฝ (db/migration.rs:db_lock_status).
|
|
377
|
+
let ws = PathBuf::from("/nonexistent-locked-fixture");
|
|
378
|
+
let outcome = repair_schema(&ws).expect("blocked repair returns Ok wrapper");
|
|
379
|
+
match outcome {
|
|
380
|
+
MigrationOutcome::Blocked { reason } => assert_eq!(reason, "active_lock"),
|
|
381
|
+
other => panic!("expected Blocked, got {other:?}"),
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
386
|
+
// diagnose_path โ bincheck.mjs printMissingBinDiagnostic ็ญไปท (typed PathDiagnostic)ใ
|
|
387
|
+
// bin ๅจ PATH โ OnPath;ไธๅจ โ NotOnPath{diagnostic}ใRust ๆ npm โ npmrc_prefix=Noneใ
|
|
388
|
+
// ็บฏ้ป่พๅฏๅๆต (็ๆข PATH ้จๅ #[ignore])ใ
|
|
389
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
390
|
+
|
|
391
|
+
#[test]
|
|
392
|
+
fn diagnose_path_when_bin_on_path_reports_on_path() {
|
|
393
|
+
// bin_dir ๅจๅฝๅ PATH โ OnPath{bin_dir}ใๆ้ :ๆ bin_dir ไธดๆถๅก่ฟ PATHใ
|
|
394
|
+
// ๅ PATH ้ฆไธช็ๅฎๆก็ฎไฝไธบ bin_dir,ไฟ่ฏใๅจ PATH ไธใใ
|
|
395
|
+
let path_var = std::env::var("PATH").unwrap_or_default();
|
|
396
|
+
let first = path_var
|
|
397
|
+
.split(':')
|
|
398
|
+
.find(|p| !p.is_empty())
|
|
399
|
+
.expect("PATH has at least one entry");
|
|
400
|
+
let bin = BinDir(PathBuf::from(first));
|
|
401
|
+
let hint = diagnose_path(&bin).expect("diagnose on-path bin");
|
|
402
|
+
match hint {
|
|
403
|
+
PathHint::OnPath { bin_dir } => assert_eq!(bin_dir, PathBuf::from(first)),
|
|
404
|
+
PathHint::NotOnPath { .. } => panic!("PATH entry should report OnPath"),
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[test]
|
|
409
|
+
fn diagnose_path_not_on_path_npmrc_prefix_is_none_no_npm() {
|
|
410
|
+
// Rust ็ๆ npm ่ทฏๅพ โ npmrc_prefix == None (skeleton:257).
|
|
411
|
+
// ็จไธไธช็ปไธๅจ PATH ็็ฎๅฝใ
|
|
412
|
+
let bin = BinDir(PathBuf::from("/zzz-definitely-not-on-path-9f3a"));
|
|
413
|
+
let hint = diagnose_path(&bin).expect("diagnose off-path bin");
|
|
414
|
+
match hint {
|
|
415
|
+
PathHint::NotOnPath { bin_dir, diagnostic } => {
|
|
416
|
+
assert_eq!(bin_dir, PathBuf::from("/zzz-definitely-not-on-path-9f3a"));
|
|
417
|
+
// Rust ๆ npm โ ็ปไธ้ๆฐๅผๅ
ฅ .npmrc ่งฃๆ.
|
|
418
|
+
assert_eq!(diagnostic.npmrc_prefix, None);
|
|
419
|
+
// path_entries == ๅฝๅ PATH ๆก็ฎๆฐ (bincheck.mjs:43).
|
|
420
|
+
let want = std::env::var("PATH")
|
|
421
|
+
.unwrap_or_default()
|
|
422
|
+
.split(':')
|
|
423
|
+
.filter(|p| !p.is_empty())
|
|
424
|
+
.count();
|
|
425
|
+
assert_eq!(diagnostic.path_entries, want);
|
|
426
|
+
}
|
|
427
|
+
PathHint::OnPath { .. } => panic!("bogus dir must be NotOnPath"),
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn path_hint_serde_tag_kind() {
|
|
433
|
+
// #[serde(tag = "kind")] โ OnPath โ {"kind":"on_path",...}.
|
|
434
|
+
let h = PathHint::OnPath {
|
|
435
|
+
bin_dir: PathBuf::from("/home/u/.local/bin"),
|
|
436
|
+
};
|
|
437
|
+
let json = serde_json::to_string(&h).unwrap();
|
|
438
|
+
assert!(json.contains("\"kind\":\"on_path\""), "got: {json}");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
442
|
+
// install_skill โ cmd_install_skill (commands.py:451-481)ใ
|
|
443
|
+
// error ่ทฏๅพ + ็ฒพ็กฎๆถๆฏ:
|
|
444
|
+
// - --dest + --target all โ InvalidOptions("--dest cannot be combined with --target all")
|
|
445
|
+
// (commands.py:453-454)
|
|
446
|
+
// dry-run ๅณ็ญ (ๆ ๅฏไฝ็จ,ๅฏๅๆต):
|
|
447
|
+
// - dry_run=true โ SkillInstallOutcome{dry_run:true, removed_stale:[]} ไธ่ฝๅฐ.
|
|
448
|
+
// ็ๆท / removed_stale #[ignore] (ๆไปถ็ณป็ปๅฏไฝ็จ)ใ
|
|
449
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
450
|
+
|
|
451
|
+
fn skill_opts(target: SkillTarget, dest: Option<PathBuf>, dry_run: bool) -> SkillInstallOptions {
|
|
452
|
+
SkillInstallOptions {
|
|
453
|
+
target,
|
|
454
|
+
dest,
|
|
455
|
+
dry_run,
|
|
456
|
+
source: PathBuf::from("/repo/skills/team-agent"),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
#[test]
|
|
461
|
+
fn install_skill_dest_with_target_all_is_invalid() {
|
|
462
|
+
// commands.py:453-454 โ `--dest cannot be combined with --target all`.
|
|
463
|
+
let opts = skill_opts(
|
|
464
|
+
SkillTarget::All,
|
|
465
|
+
Some(PathBuf::from("/custom/dest")),
|
|
466
|
+
false,
|
|
467
|
+
);
|
|
468
|
+
let err = install_skill(&opts).expect_err("dest + all must error");
|
|
469
|
+
match err {
|
|
470
|
+
PackagingError::InvalidOptions(msg) => assert!(
|
|
471
|
+
msg.contains("--dest cannot be combined with --target all"),
|
|
472
|
+
"got: {msg}"
|
|
473
|
+
),
|
|
474
|
+
other => panic!("expected InvalidOptions, got {other:?}"),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#[test]
|
|
479
|
+
fn install_skill_dry_run_single_target_reports_plan_no_side_effects() {
|
|
480
|
+
// commands.py:477-478 dry_run โ {ok, source, dest, dry_run:true} ไธ่ฝๅฐ.
|
|
481
|
+
let opts = skill_opts(SkillTarget::Codex, None, true);
|
|
482
|
+
let outcomes = install_skill(&opts).expect("dry-run install-skill");
|
|
483
|
+
assert_eq!(outcomes.len(), 1, "single target โ 1 outcome");
|
|
484
|
+
let o = &outcomes[0];
|
|
485
|
+
assert_eq!(o.target, SkillTarget::Codex);
|
|
486
|
+
assert!(o.dry_run, "dry_run flag preserved");
|
|
487
|
+
// dry-run ็ปไธๆธ
็ๆฎ็ (ๆ ๅฏไฝ็จ).
|
|
488
|
+
assert!(o.removed_stale.is_empty(), "dry-run touches nothing");
|
|
489
|
+
// dest == codex skill dir (HOME ไพ่ต,็จ dirs ่งฃๆ;ๅชๆญๅ็ผ็จณๅฎ้จๅ).
|
|
490
|
+
assert!(
|
|
491
|
+
o.dest
|
|
492
|
+
.0
|
|
493
|
+
.ends_with(PathBuf::from(".codex/skills/team-agent")),
|
|
494
|
+
"got: {:?}",
|
|
495
|
+
o.dest
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#[test]
|
|
500
|
+
fn install_skill_dry_run_target_all_fans_out_to_two() {
|
|
501
|
+
// commands.py:458-463 โ target all โ ไธคไธช outcome (codex + claude),้กบๅบๅบๅฎ.
|
|
502
|
+
let opts = skill_opts(SkillTarget::All, None, true);
|
|
503
|
+
let outcomes = install_skill(&opts).expect("dry-run install-skill all");
|
|
504
|
+
assert_eq!(outcomes.len(), 2, "all โ fan-out codex+claude");
|
|
505
|
+
// KEY ORDER:commands.py:460-461 codex first, claude second.
|
|
506
|
+
assert_eq!(outcomes[0].target, SkillTarget::Codex);
|
|
507
|
+
assert_eq!(outcomes[1].target, SkillTarget::Claude);
|
|
508
|
+
assert!(outcomes.iter().all(|o| o.dry_run));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#[test]
|
|
512
|
+
fn install_skill_dry_run_explicit_dest_single_target() {
|
|
513
|
+
// commands.py:455-457 โ --dest ๆพๅผ็ฎๅฝ,่ฆ็ target ่ทฏๅพ่งฃๆ,ๅ outcome.
|
|
514
|
+
let dest = PathBuf::from("/custom/skills/team-agent");
|
|
515
|
+
let opts = skill_opts(SkillTarget::Codex, Some(dest.clone()), true);
|
|
516
|
+
let outcomes = install_skill(&opts).expect("dry-run explicit dest");
|
|
517
|
+
assert_eq!(outcomes.len(), 1);
|
|
518
|
+
assert_eq!(outcomes[0].dest, SkillDestDir(dest));
|
|
519
|
+
assert!(outcomes[0].dry_run);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#[test]
|
|
523
|
+
#[ignore = "REAL-MACHINE-E2E: real copytree + stale diff removal (fixes dirs_exist_ok=True residue)"]
|
|
524
|
+
fn install_skill_real_copy_removes_stale_files() {
|
|
525
|
+
// ไฟฎ commands.py:480 dirs_exist_ok ๆฎ็:Rust ๆทๅๆธ
ๆง SKILL,่ฎฐๅฝ removed_stale.
|
|
526
|
+
let opts = skill_opts(SkillTarget::Codex, Some(PathBuf::from("/tmp/ta-skill-real")), false);
|
|
527
|
+
let outcomes = install_skill(&opts).expect("real install-skill");
|
|
528
|
+
assert!(!outcomes[0].dry_run);
|
|
529
|
+
// ็่ทฏๅพไธ่ฅๆๆงๆฎ็,removed_stale ้็ฉบ (ๅ
ทไฝๅผไพ fixture).
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
533
|
+
// uninstall โ install.mjs:109-130ใๅฎๅ
จๆคๆ :
|
|
534
|
+
// - ้ป่ฎค purge_runtime=false โ purged_runtime=false,ไธๅ workspace/.team.
|
|
535
|
+
// - purge_runtime=true ไธๆฃๆตๆ team ๅจ่ท โ ๆ็ป:purge_refused_team_running=true
|
|
536
|
+
// (ๆ่ฟ PurgeRefusedTeamRunning err โ ๅๅฎ็ฐ็บชๅพ,่ฟ้้ใไธ็ๅ ใ)ใ
|
|
537
|
+
// ็ๅ / team-running ๅคๅฎ #[ignore]ใ
|
|
538
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
539
|
+
|
|
540
|
+
/// TEST-SUPPORT seed helper: build a workspace whose state.json projects a RUNNING team
|
|
541
|
+
/// (status "running" + a live coordinator pid). The team-running guard reads this projection.
|
|
542
|
+
/// Real impl (pure scaffolding, not a production fn) so the NEW-CONTRACT guard test has a
|
|
543
|
+
/// concrete fixture instead of a bare nonexistent path.
|
|
544
|
+
fn seed_workspace_with_running_team(tag: &str) -> PathBuf {
|
|
545
|
+
let ws = std::env::temp_dir().join(format!("ta-ws-running-{}-{}", std::process::id(), tag));
|
|
546
|
+
let team_dir = ws.join(".team");
|
|
547
|
+
std::fs::create_dir_all(&team_dir).expect("seed .team dir");
|
|
548
|
+
// state.json shape mirrors step-5 state projection: a team marked running.
|
|
549
|
+
let state = serde_json::json!({
|
|
550
|
+
"teams": {
|
|
551
|
+
"demo": { "status": "running", "coordinator_pid": std::process::id() }
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
std::fs::write(
|
|
555
|
+
team_dir.join("state.json"),
|
|
556
|
+
serde_json::to_vec_pretty(&state).unwrap(),
|
|
557
|
+
)
|
|
558
|
+
.expect("seed state.json");
|
|
559
|
+
ws
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// TEST-SUPPORT seed helper: an idle workspace (state.json present, no running team).
|
|
563
|
+
fn seed_workspace_idle(tag: &str) -> PathBuf {
|
|
564
|
+
let ws = std::env::temp_dir().join(format!("ta-ws-idle-{}-{}", std::process::id(), tag));
|
|
565
|
+
let team_dir = ws.join(".team");
|
|
566
|
+
std::fs::create_dir_all(&team_dir).expect("seed .team dir");
|
|
567
|
+
std::fs::write(
|
|
568
|
+
team_dir.join("state.json"),
|
|
569
|
+
serde_json::to_vec_pretty(&serde_json::json!({ "teams": {} })).unwrap(),
|
|
570
|
+
)
|
|
571
|
+
.expect("seed state.json");
|
|
572
|
+
ws
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#[test]
|
|
576
|
+
#[serial_test::serial(env)]
|
|
577
|
+
fn uninstall_default_does_not_purge_runtime() {
|
|
578
|
+
// PORT-GOLDEN (install.mjs:127-129): default (no --purge-runtime) leaves runtime; the
|
|
579
|
+
// "runtime directories are left ... for rollback" branch. Default must NOT purge
|
|
580
|
+
// workspace/.team. With workspace seeded, default must STILL leave it untouched on disk.
|
|
581
|
+
// Isolate HOME: uninstall() now removes ~/.codex|.claude skill dirs (reads HOME). Without an
|
|
582
|
+
// isolated empty HOME this test would (a) delete the real user's skill dir and (b) race
|
|
583
|
+
// p2_uninstall_removes_both_skill_dirs' HOME mutation โ remove_dir_all NotFound flake under
|
|
584
|
+
// parallel cargo. Shared ENV_LOCK_PKG serializes the two HOME-touching uninstall tests.
|
|
585
|
+
let _g = ENV_LOCK_PKG.lock().unwrap_or_else(|p| p.into_inner());
|
|
586
|
+
let home = std::env::temp_dir().join(format!("ta-uninst-default-home-{}", std::process::id()));
|
|
587
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
588
|
+
let _h = HomeGuard::set(&home);
|
|
589
|
+
let ws = seed_workspace_idle("default-nopurge");
|
|
590
|
+
let opts = UninstallOptions {
|
|
591
|
+
prefix: Prefix(std::env::temp_dir().join(format!("ta-uninst-{}", std::process::id()))),
|
|
592
|
+
purge_runtime: false,
|
|
593
|
+
workspace: Some(ws.clone()),
|
|
594
|
+
};
|
|
595
|
+
let outcome = uninstall(&opts).expect("default uninstall");
|
|
596
|
+
assert!(!outcome.purged_runtime, "default must NOT purge runtime");
|
|
597
|
+
assert!(
|
|
598
|
+
!outcome.purge_refused_team_running,
|
|
599
|
+
"no purge requested โ no refusal"
|
|
600
|
+
);
|
|
601
|
+
// SAFETY INVARIANT (card ยงuninstall ็ปไธ้ป่ฎคๅ workspace/.team): the seeded workspace
|
|
602
|
+
// .team must still exist after a default uninstall.
|
|
603
|
+
assert!(
|
|
604
|
+
ws.join(".team").join("state.json").exists(),
|
|
605
|
+
"default uninstall must NEVER delete workspace/.team"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[test]
|
|
610
|
+
#[ignore = "NEW-CONTRACT (Rust hardening, NOT a Python port-golden): real team-running guard \
|
|
611
|
+
needs live state projection (step 5). Python install.mjs:123-128 has NO such guard \
|
|
612
|
+
โ it purges unconditionally on --purge-runtime. Confirmed with gate w59ds828k: this \
|
|
613
|
+
is intentional hardening backed by card ยงuninstall prose 'pass --purge-runtime only \
|
|
614
|
+
when no teams are running.' Marked NEW-CONTRACT, not PORT."]
|
|
615
|
+
fn uninstall_purge_refused_when_team_running_NEW_CONTRACT() {
|
|
616
|
+
// NEW-CONTRACT safety guard: purge_runtime=true but workspace projects a RUNNING team โ
|
|
617
|
+
// REFUSE purge (purge_refused_team_running=true OR PurgeRefusedTeamRunning err). The
|
|
618
|
+
// seeded workspace .team MUST survive regardless. This behavior is NOT in install.mjs.
|
|
619
|
+
let ws = seed_workspace_with_running_team("guard");
|
|
620
|
+
let opts = UninstallOptions {
|
|
621
|
+
prefix: Prefix(std::env::temp_dir().join("ta-uninst-guard")),
|
|
622
|
+
purge_runtime: true,
|
|
623
|
+
workspace: Some(ws.clone()),
|
|
624
|
+
};
|
|
625
|
+
match uninstall(&opts) {
|
|
626
|
+
Ok(o) => {
|
|
627
|
+
assert!(!o.purged_runtime, "must not purge while team running");
|
|
628
|
+
assert!(o.purge_refused_team_running, "must set refusal flag");
|
|
629
|
+
}
|
|
630
|
+
Err(PackagingError::PurgeRefusedTeamRunning(refused_ws)) => {
|
|
631
|
+
assert_eq!(refused_ws, ws, "refusal must name the running workspace");
|
|
632
|
+
}
|
|
633
|
+
Err(other) => panic!("expected refusal, got {other:?}"),
|
|
634
|
+
}
|
|
635
|
+
// Hard invariant: refused purge must leave the workspace fully intact.
|
|
636
|
+
assert!(
|
|
637
|
+
ws.join(".team").join("state.json").exists(),
|
|
638
|
+
"refused purge must NEVER delete workspace/.team"
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#[test]
|
|
643
|
+
#[ignore = "REAL-MACHINE-E2E: PORT-GOLDEN โ --purge-runtime with NO running team really removes \
|
|
644
|
+
the runtime root (install.mjs:123-126 unconditional rmSync). File-system side effect."]
|
|
645
|
+
fn uninstall_purge_runtime_idle_workspace_purges_PORT_GOLDEN() {
|
|
646
|
+
// PORT-GOLDEN (install.mjs:123-126): --purge-runtime DOES purge when no team is running.
|
|
647
|
+
// This is the faithful Python behavior (the guard above is the only Rust addition).
|
|
648
|
+
let ws = seed_workspace_idle("port-purge");
|
|
649
|
+
let opts = UninstallOptions {
|
|
650
|
+
prefix: Prefix(std::env::temp_dir().join("ta-uninst-port-purge")),
|
|
651
|
+
purge_runtime: true,
|
|
652
|
+
workspace: Some(ws.clone()),
|
|
653
|
+
};
|
|
654
|
+
let outcome = uninstall(&opts).expect("purge on idle workspace");
|
|
655
|
+
assert!(outcome.purged_runtime, "idle + --purge-runtime โ purged");
|
|
656
|
+
assert!(!outcome.purge_refused_team_running, "no team โ no refusal");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
660
|
+
// install / update โ install.mjs:48-95ใ
|
|
661
|
+
// - install ้ฆ่ฃ
:InstallReport.replace == None (ๆ ไบ่ฟๅถๆฟๆข).
|
|
662
|
+
// - update:replace == Some(..) (ๆๅๅญๆฟๆข;ๅคฑ่ดฅๅๆปๅฐ .previous,bug-084 ๅๆบ).
|
|
663
|
+
// - installer ้ป่ฎค skill_target = All (install.mjs:74 `--target all`).
|
|
664
|
+
// ๅ
จๅฏไฝ็จ #[ignore] (ๅ bin / ๆท skill / ๆข PATH / ่ท doctor) โ clean-install E2E.
|
|
665
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
666
|
+
|
|
667
|
+
fn install_opts(skill_target: SkillTarget) -> InstallOptions {
|
|
668
|
+
InstallOptions {
|
|
669
|
+
prefix: Prefix(PathBuf::from("/home/u/.local")),
|
|
670
|
+
self_binary: PathBuf::from("/proc/self/exe"),
|
|
671
|
+
skill_target,
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
#[test]
|
|
676
|
+
#[ignore = "REAL-MACHINE-E2E: clean-install writes bin + copies skill + runs doctor + probes PATH"]
|
|
677
|
+
fn install_first_time_has_no_binary_replace() {
|
|
678
|
+
// install.mjs:48 install ๅ
ฅๅฃ โ ้ฆ่ฃ
ๆ replace.
|
|
679
|
+
let opts = install_opts(SkillTarget::All);
|
|
680
|
+
let report = install(&opts).expect("clean install");
|
|
681
|
+
assert!(report.replace.is_none(), "first install must NOT replace");
|
|
682
|
+
// installer ้ป่ฎค่ฃ
ไธคไธช skill (--target all).
|
|
683
|
+
assert_eq!(report.skills.len(), 2);
|
|
684
|
+
// ็ๆฌ == ๅไธ็็ธๆบ.
|
|
685
|
+
assert_eq!(report.version, Version::current());
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
#[test]
|
|
689
|
+
#[ignore = "REAL-MACHINE-E2E: atomic binary replace + .previous backup + rollback (bug-084 ๅๆบ)"]
|
|
690
|
+
fn update_performs_atomic_binary_replace() {
|
|
691
|
+
// install.mjs:60-66 โ update ๆ destโbackup + tmpโdest ๅๅญๆฟๆข.
|
|
692
|
+
let opts = install_opts(SkillTarget::All);
|
|
693
|
+
let report = update(&opts).expect("update");
|
|
694
|
+
match report.replace {
|
|
695
|
+
Some(AtomicReplaceOutcome::Replaced { .. })
|
|
696
|
+
| Some(AtomicReplaceOutcome::ReplacedCrossDevice { .. }) => {}
|
|
697
|
+
other => panic!("update must replace binary, got {other:?}"),
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
#[test]
|
|
702
|
+
fn atomic_replace_outcome_serde_tag_outcome() {
|
|
703
|
+
// #[serde(tag = "outcome")] โ RolledBack carries restored_from + error.
|
|
704
|
+
let o = AtomicReplaceOutcome::RolledBack {
|
|
705
|
+
restored_from: PathBuf::from("/home/u/.local/bin/.previous"),
|
|
706
|
+
error: "EXDEV".to_string(),
|
|
707
|
+
};
|
|
708
|
+
let json = serde_json::to_string(&o).unwrap();
|
|
709
|
+
assert!(json.contains("\"outcome\":\"rolled_back\""), "got: {json}");
|
|
710
|
+
assert!(json.contains("\"error\":\"EXDEV\""), "got: {json}");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
714
|
+
// ยง84 โ packaging ็ปไธ่งฆๅ provider client / prompt / token.
|
|
715
|
+
// install_skill ๅชๆทๆไปถ (provider ่ฐ็จ่ฎกๆฐ = 0)ใๆญคๅคไปฅใๆ provider ไพ่ตใ
|
|
716
|
+
// ็็ปๆๆงๆญ่จไปฃๆฟ่ฟ่กๆถ่ฎกๆฐ:dry-run install-skill ไธๅบ้่ฆไปปไฝ provider optsใ
|
|
717
|
+
// (็ mock-provider-call-count==0 ๆญ่จๅฝ้ๆๅฑ,่ฟ้้ dry-run ไธ่ฏป provider.)
|
|
718
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
719
|
+
|
|
720
|
+
#[test]
|
|
721
|
+
fn install_skill_dry_run_is_pure_no_provider_state() {
|
|
722
|
+
// ยง84:install-skill ๅชๆทๆไปถ;dry-run ่ฟๆไปถ้ฝไธๅจ โ ็บฏๅฝๆฐๅผๅฏ้ๅค.
|
|
723
|
+
let opts = skill_opts(SkillTarget::Claude, None, true);
|
|
724
|
+
let first = install_skill(&opts).expect("dry-run 1");
|
|
725
|
+
let second = install_skill(&opts).expect("dry-run 2");
|
|
726
|
+
assert_eq!(first, second, "dry-run install-skill must be deterministic & side-effect free");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
730
|
+
// CRLF / platform โ wrapper ๅ
ๅฎนๅจ Python ๆฏ sh wrapper (LF)ใRust ๅไบ่ฟๅถ
|
|
731
|
+
// ๅๆ sh wrapper;ไฝ PATH ่ฏๆญ็ path_entries ๅ้ๅจ Windows ็จ ';'ใ
|
|
732
|
+
// ๆญคๅค้ diagnose_path ๅจ็ฉบ PATH ๆถ path_entries==0 (bincheck.mjs:43 ไธๅ
)ใ
|
|
733
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
734
|
+
|
|
735
|
+
#[test]
|
|
736
|
+
#[ignore = "REAL-MACHINE-E2E: needs to override process PATH env to empty/Windows-delimited"]
|
|
737
|
+
fn diagnose_path_empty_path_has_zero_entries() {
|
|
738
|
+
// bincheck.mjs:43 โ searchPath ? split.length : 0;็ฉบ PATH โ 0 entries.
|
|
739
|
+
// (็ๆน process env PATH ๅฝฑๅๅนถ่กๆต่ฏ,ๆ
ignore;ๅฎ็ฐๅฑๅบๆฏๆๆณจๅ
ฅ PATH.)
|
|
740
|
+
let bin = BinDir(PathBuf::from("/anything"));
|
|
741
|
+
let hint = diagnose_path(&bin).expect("diagnose empty path");
|
|
742
|
+
if let PathHint::NotOnPath { diagnostic, .. } = hint {
|
|
743
|
+
assert_eq!(diagnostic.path_entries, 0);
|
|
744
|
+
} else {
|
|
745
|
+
panic!("empty PATH โ NotOnPath");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// โโโโโโโโโโโโโโโ P2 FIX-LOOP RED (ๅค็ปฟๅณๅฏนๆ cross-model findings) โโโโโโโโโโโโโโโ
|
|
750
|
+
|
|
751
|
+
static ENV_LOCK_PKG: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
752
|
+
struct HomeGuard {
|
|
753
|
+
prev: Option<String>,
|
|
754
|
+
}
|
|
755
|
+
impl HomeGuard {
|
|
756
|
+
fn set(home: &Path) -> Self {
|
|
757
|
+
let prev = std::env::var("HOME").ok();
|
|
758
|
+
std::env::set_var("HOME", home);
|
|
759
|
+
Self { prev }
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
impl Drop for HomeGuard {
|
|
763
|
+
fn drop(&mut self) {
|
|
764
|
+
match &self.prev {
|
|
765
|
+
Some(v) => std::env::set_var("HOME", v),
|
|
766
|
+
None => std::env::remove_var("HOME"),
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// P1 โ update() must perform a REAL atomic replace (rename destโ.previous), not fabricate
|
|
772
|
+
// a Replaced outcome whose backup file never exists (install.mjs:60-66; bug-084).
|
|
773
|
+
#[test]
|
|
774
|
+
fn p2_update_creates_real_atomic_replace_backup() {
|
|
775
|
+
let base = std::env::temp_dir().join(format!("ta-p2-update-{}", std::process::id()));
|
|
776
|
+
let prefix = base.join("prefix");
|
|
777
|
+
std::fs::create_dir_all(prefix.join("bin")).unwrap();
|
|
778
|
+
let dest = prefix.join("bin").join("team-agent");
|
|
779
|
+
std::fs::write(&dest, b"OLD BINARY").unwrap(); // pre-existing bin to back up
|
|
780
|
+
let self_bin = base.join("team-agent-new");
|
|
781
|
+
std::fs::write(&self_bin, b"NEW BINARY").unwrap();
|
|
782
|
+
|
|
783
|
+
let opts = InstallOptions {
|
|
784
|
+
prefix: Prefix(prefix.clone()),
|
|
785
|
+
self_binary: self_bin,
|
|
786
|
+
skill_target: SkillTarget::All,
|
|
787
|
+
};
|
|
788
|
+
let report = update(&opts).unwrap();
|
|
789
|
+
let backup = match report.replace {
|
|
790
|
+
Some(AtomicReplaceOutcome::Replaced { backup }) => backup,
|
|
791
|
+
other => panic!("update must report a Replaced atomic replace, got {other:?}"),
|
|
792
|
+
};
|
|
793
|
+
assert!(
|
|
794
|
+
backup.exists(),
|
|
795
|
+
"update() must actually rename destโ.previous; the claimed backup file must exist on disk"
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// P1 โ uninstall() must remove BOTH ~/.codex/skills/team-agent and ~/.claude/skills/team-agent
|
|
800
|
+
// and record them (install.mjs:115-122). Current returns removed_skill_dirs empty and leaves
|
|
801
|
+
// the dirs on disk.
|
|
802
|
+
#[test]
|
|
803
|
+
#[serial_test::serial(env)]
|
|
804
|
+
fn p2_uninstall_removes_both_skill_dirs() {
|
|
805
|
+
let _g = ENV_LOCK_PKG.lock().unwrap_or_else(|p| p.into_inner());
|
|
806
|
+
let base = std::env::temp_dir().join(format!("ta-p2-uninst-{}", std::process::id()));
|
|
807
|
+
let home = base.join("home");
|
|
808
|
+
let codex = home.join(".codex").join("skills").join("team-agent");
|
|
809
|
+
let claude = home.join(".claude").join("skills").join("team-agent");
|
|
810
|
+
std::fs::create_dir_all(&codex).unwrap();
|
|
811
|
+
std::fs::create_dir_all(&claude).unwrap();
|
|
812
|
+
std::fs::write(codex.join("SKILL.md"), b"x").unwrap();
|
|
813
|
+
std::fs::write(claude.join("SKILL.md"), b"x").unwrap();
|
|
814
|
+
let _h = HomeGuard::set(&home);
|
|
815
|
+
|
|
816
|
+
let opts = UninstallOptions {
|
|
817
|
+
prefix: Prefix(base.join("prefix")),
|
|
818
|
+
purge_runtime: false,
|
|
819
|
+
workspace: None,
|
|
820
|
+
};
|
|
821
|
+
let out = uninstall(&opts).unwrap();
|
|
822
|
+
assert_eq!(
|
|
823
|
+
out.removed_skill_dirs.len(),
|
|
824
|
+
2,
|
|
825
|
+
"uninstall must remove BOTH ~/.codex and ~/.claude skill dirs"
|
|
826
|
+
);
|
|
827
|
+
assert!(!codex.exists(), "~/.codex skill dir must be removed");
|
|
828
|
+
assert!(!claude.exists(), "~/.claude skill dir must be removed");
|
|
829
|
+
}
|