//! Do a mission agent's pinned skills actually reach its prompt? //! //! Before this test the honest answer was no, for every skill and every role. //! The catalogue's only delivery channel was the `clawmates_skills` MCP server, //! and a mission claw could not reach it: `provision_claw` wrote a constant //! bundle list, the runtime config defines no such bundle, and mission claws //! run on `claude_cli`, which is text-only and cannot surface a tool call. //! //! So the skills were authored, bound, listed in the boot log as bound — and //! structurally unreadable. That is why this is a test and not a comment: the //! failure produced no error anywhere, and every layer reported success. use cm_api::skill_delivery::Mode; use cm_api::topology_exec::{MissionTap, ZeroClawDriveExecutor}; use cm_domain::{Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId}; /// `agents.managed_by` is a real FK, so the owner has to exist. async fn seed_user(pool: &sqlx::PgPool, ws: WorkspaceId) -> UserId { let user = User { id: UserId::new(), workspace_id: ws, email: format!("owner-{}@example.com", Uuid::now_v7().simple()), role: Role::Owner, display_name: "Owner".into(), created_at: time::OffsetDateTime::UNIX_EPOCH, }; cm_db::repo::users::insert(pool, &user).await.unwrap(); user.id } use std::collections::HashMap; use uuid::Uuid; /// A claw with one template-bound, pinned skill. Returns its runtime alias. async fn seed_claw_with_pinned_skill(pool: &sqlx::PgPool, body: &str) -> (String, WorkspaceId) { let ws = Workspace { id: WorkspaceId::new(), name: "Skill Delivery Test".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(pool, &ws).await.unwrap(); let user = seed_user(pool, ws.id).await; let agent = Agent { id: AgentId::new(), workspace_id: ws.id, name: "Scout".into(), job_title: "researcher".into(), system_prompt: String::new(), avatar: String::new(), accent: String::new(), wallpaper: String::new(), managed_by: user, status: AgentStatus::Online, }; cm_db::repo::agents::insert(pool, &agent, &cm_domain::AccessPolicy::default()) .await .unwrap(); // A template with one role, and a skill pinned to it. let template_id = Uuid::now_v7(); sqlx::query( "INSERT INTO team_templates (id, key, name, description, category, stack, default_topology, risk_profile, mcp_bundles, version) VALUES ($1, $2, 'Delivery Test', 'test', 'research', '{}', 'pipeline', 'research_readonly', '{}', 1)", ) .bind(template_id) .bind(format!("delivery_test_{}", template_id.simple())) .execute(pool) .await .unwrap(); sqlx::query( "INSERT INTO template_roles (template_id, slot, order_idx, system_prompt) VALUES ($1, 'researcher', 0, 'you research')", ) .bind(template_id) .execute(pool) .await .unwrap(); let skill_id = Uuid::now_v7(); sqlx::query( "INSERT INTO skills (id, workspace_id, name, title, author, description, when_to_use, tags, source_kind, current_version, body) VALUES ($1, NULL, $2, $2, 'system', 'a procedure the agent must follow', 'always', '{}', 'builtin', 1, $3)", ) .bind(skill_id) .bind(format!("delivery-test-skill-{}", skill_id.simple())) .bind(body) .execute(pool) .await .unwrap(); sqlx::query( "INSERT INTO template_role_skills (template_id, slot, skill_id, pin_in_context, order_idx) VALUES ($1, 'researcher', $2, true, 0)", ) .bind(template_id) .bind(skill_id) .execute(pool) .await .unwrap(); cm_db::repo::agent_template_link::upsert( pool, agent.id.as_uuid(), template_id, 1, "researcher", ) .await .unwrap(); ( cm_api::runtime_provision::claw_alias(agent.id.as_uuid()), ws.id, ) } fn executor(pool: &sqlx::PgPool, workspace_id: WorkspaceId) -> ZeroClawDriveExecutor { executor_for(pool, workspace_id, Uuid::now_v7()) } fn executor_for( pool: &sqlx::PgPool, workspace_id: WorkspaceId, mission_id: Uuid, ) -> ZeroClawDriveExecutor { ZeroClawDriveExecutor::new( "http://127.0.0.1:1".into(), "unused".into(), HashMap::new(), "default".into(), ) .with_tap(MissionTap { pool: pool.clone(), workspace_id: workspace_id.as_uuid(), mission_id, phase_id: None, run_id: None, }) } /// A mission row carrying an explicit delivery arm. async fn seed_mission_on_arm(pool: &sqlx::PgPool, ws: WorkspaceId, arm: Option<&str>) -> Uuid { let mission = Uuid::now_v7(); sqlx::query( "INSERT INTO missions (id, workspace_id, title, template_kind, status, skill_delivery) VALUES ($1, $2, 'arm', 'research_only', 'running', $3)", ) .bind(mission) .bind(ws.as_uuid()) .bind(arm) .execute(pool) .await .unwrap(); mission } // ── The index arm ─────────────────────────────────────────────────── // // Trigger — did the agent reach for the skill? — cannot exist while every body // is handed over unasked. These tests cover the arm that makes it a question, // and the one guarantee the control arm needs: that it did not change. #[tokio::test] async fn the_index_arm_sends_the_uri_and_withholds_the_body() { let pool = cm_testkit::test_pool().await; const MARKER: &str = "Never review a paper from its title alone."; let (alias, ws) = seed_claw_with_pinned_skill(&pool, MARKER).await; let mission = seed_mission_on_arm(&pool, ws, Some("index")).await; let text = executor_for(&pool, ws, mission) .pinned_skills_text(&alias) .await .expect("an indexed skill is still delivered — as an entry, not a body"); assert!( !text.contains(MARKER), "the BODY is what the index withholds; leaving it in delivers both \ arms at once and measures neither. Got:\n{text}" ); assert!( text.contains("uri=\"skill:global/delivery-test-skill-"), "an entry without a fetchable uri names a procedure the agent cannot \ obtain — worse than inlining it. Got:\n{text}" ); assert!( text.contains("When to use: always"), "`when_to_use` is the only thing the agent can judge relevance from, \ and judging relevance is the entire axis. Got:\n{text}" ); // The scorer counts delivered skills by the marker, in both arms. assert_eq!( cm_api::topology_exec::skill_names_in(&text).len(), 1, "an indexed skill must still count as delivered:\n{text}" ); } #[tokio::test] async fn an_unrecorded_arm_delivers_bodies() { let pool = cm_testkit::test_pool().await; const MARKER: &str = "Never review a paper from its title alone."; let (alias, ws) = seed_claw_with_pinned_skill(&pool, MARKER).await; // Every mission that ran before the column existed, plus any row whose // value is unreadable, plus a turn with no mission row at all. All three // resolve to the arm that needs nothing installed to work. for arm in [None, Some("nonsense")] { let mission = seed_mission_on_arm(&pool, ws, arm).await; let text = executor_for(&pool, ws, mission) .pinned_skills_text(&alias) .await .unwrap(); assert!( text.contains(MARKER), "skill_delivery={arm:?} must deliver the body — an index arm \ selected by accident hands out uris behind a door that may not \ be installed. Got:\n{text}" ); } } #[tokio::test] async fn a_pinned_skill_body_reaches_the_turn_prompt() { let pool = cm_testkit::test_pool().await; const MARKER: &str = "Never review a paper from its title alone."; let (alias, ws) = seed_claw_with_pinned_skill(&pool, MARKER).await; let text = executor(&pool, ws) .pinned_skills_text(&alias) .await .expect("a claw with a pinned template skill must produce skill text"); assert!( text.contains(MARKER), "the default arm delivers the BODY. It is the control in the delivery \ A/B, so it has to stay what production has always sent; the index arm \ is selected per mission and tested separately. Got:\n{text}" ); } #[tokio::test] async fn an_agent_with_no_pinned_skills_adds_nothing() { let pool = cm_testkit::test_pool().await; let ws = Workspace { id: WorkspaceId::new(), name: "No Skills".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); let user = seed_user(&pool, ws.id).await; let agent = Agent { id: AgentId::new(), workspace_id: ws.id, name: "Bare".into(), job_title: "researcher".into(), system_prompt: String::new(), avatar: String::new(), accent: String::new(), wallpaper: String::new(), managed_by: user, status: AgentStatus::Online, }; cm_db::repo::agents::insert(&pool, &agent, &cm_domain::AccessPolicy::default()) .await .unwrap(); let alias = cm_api::runtime_provision::claw_alias(agent.id.as_uuid()); assert!( executor(&pool, ws.id) .pinned_skills_text(&alias) .await .is_none(), "an agent with no bound skills must add no section at all — an empty \ `# Your skills` heading tells the model it has skills and then shows \ it none" ); } #[test] fn the_composed_prompt_carries_the_skill_and_omits_the_heading_when_empty() { let base = "You are the \"researcher\" agent.\n\nTask: read the papers"; let with = cm_api::topology_exec::compose_turn_prompt(base, Some("## arxiv-daily\nDo not re-search."), Mode::Inline); assert!(with.contains("Task: read the papers"), "the base turn must survive"); assert!(with.contains("Do not re-search."), "the skill body must be in the prompt"); assert!(with.contains("# Your skills"), "the section needs a heading"); for empty in [None, Some(""), Some(" \n ")] { let without = cm_api::topology_exec::compose_turn_prompt(base, empty, Mode::Inline); assert_eq!( without, base, "with no skills the prompt must be byte-identical to the base — an \ empty heading announces skills the agent does not have" ); } } // ── The other three tiers ─────────────────────────────────────────── // // `topology_exec` injects per TURN and covers only the container tier. The // composed-microVM, solo-microVM and direct-session paths in `phase_runner` // share one task string built by `phase_task_text`, and until now that string // carried no skill at all — so a mission on any of those tiers ran with the // catalogue unreachable, exactly as the container tier did before e4942ce. /// Bind a claw to a mission's crew so `phase_skills_text` can find it. async fn seed_mission_with_crew(pool: &sqlx::PgPool, ws: WorkspaceId, agent: AgentId) -> Uuid { let mission = Uuid::now_v7(); sqlx::query( "INSERT INTO missions (id, workspace_id, title, template_kind, status) VALUES ($1, $2, 'skill delivery', 'research_only', 'running')", ) .bind(mission) .bind(ws.as_uuid()) .execute(pool) .await .unwrap(); let team = Uuid::now_v7(); sqlx::query( "INSERT INTO teams (id, workspace_id, name, kind, lifecycle, graph) VALUES ($1, $2, 'crew', 'pipeline', 'permanent', '{}'::jsonb)", ) .bind(team) .bind(ws.as_uuid()) .execute(pool) .await .unwrap(); sqlx::query("INSERT INTO team_members (team_id, claw_id, node_id, role) VALUES ($1, $2, 'researcher', 'researcher')") .bind(team) .bind(agent.as_uuid()) .execute(pool) .await .unwrap(); sqlx::query("INSERT INTO mission_teams (mission_id, team_id, purpose) VALUES ($1, $2, 'mission')") .bind(mission) .bind(team) .execute(pool) .await .unwrap(); mission } #[tokio::test] async fn the_microvm_and_session_tiers_get_the_skill_in_their_task_text() { let pool = cm_testkit::test_pool().await; const MARKER: &str = "Never review a paper from its title alone."; let (alias, ws) = seed_claw_with_pinned_skill(&pool, MARKER).await; let agent = AgentId::from(cm_api::runtime_provision::claw_from_alias(&alias).unwrap()); let mission = seed_mission_with_crew(&pool, ws, agent).await; let skills = cm_api::phase_runner::phase_skills_text(&pool, mission) .await .expect("a mission whose crew holds a pinned skill must produce skill text"); assert!( skills.contains(MARKER), "the pinned BODY must reach the phase task — these tiers run one \ `claude -p` session with no per-turn injection, so this string is the \ agent's only route to the procedure. Got:\n{skills}" ); // All three tiers share this composition, so testing it once covers them. let composed = cm_api::topology_exec::compose_turn_prompt("Task: read the papers", Some(&skills), Mode::Inline); assert!(composed.contains(MARKER)); assert!(composed.contains("Task: read the papers")); } #[tokio::test] async fn a_mission_whose_crew_has_no_skills_adds_nothing() { let pool = cm_testkit::test_pool().await; let ws = Workspace { id: WorkspaceId::new(), name: "Bare Crew".into(), plan: "team".into(), }; cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); let user = seed_user(&pool, ws.id).await; let agent = Agent { id: AgentId::new(), workspace_id: ws.id, name: "Bare".into(), job_title: "researcher".into(), system_prompt: String::new(), avatar: String::new(), accent: String::new(), wallpaper: String::new(), managed_by: user, status: AgentStatus::Online, }; cm_db::repo::agents::insert(&pool, &agent, &cm_domain::AccessPolicy::default()) .await .unwrap(); let mission = seed_mission_with_crew(&pool, ws.id, agent.id).await; assert!( cm_api::phase_runner::phase_skills_text(&pool, mission) .await .is_none(), "a crew with no pinned skills must add no section — the empty-heading \ rule has to hold on this path too" ); }