| 1 | use super::*; |
| 2 | |
| 3 | #[tokio::test] |
| 4 | async fn get_v1_workspace_instructions_lists_effective_sources() -> Result<()> { |
| 5 | // No `lock_test_env`/`EnvVarGuard` here: the handler resolves the global |
| 6 | // layer's home inside `spawn_blocking`, and a blocking-pool thread is not |
| 7 | // enrolled in the test's env scope — sealing the environment would |
| 8 | // deadlock the request against the lock this test would be holding. |
| 9 | let temp = tempfile::tempdir()?; |
| 10 | let root = temp.path().join("instructions-route"); |
| 11 | let sessions_dir = root.join("sessions"); |
| 12 | let workspace = root.join("workspace"); |
| 13 | fs::create_dir_all(workspace.join(".git"))?; |
| 14 | fs::write( |
| 15 | workspace.join(".git").join("HEAD"), |
| 16 | "ref: refs/heads/main\n", |
| 17 | )?; |
| 18 | fs::write(workspace.join("AGENTS.md"), "# Repo rules\n")?; |
| 19 | // Exists but shadowed: AGENTS.md outranks it in the same directory. |
| 20 | fs::create_dir_all(workspace.join(".codewhale"))?; |
| 21 | fs::write( |
| 22 | workspace.join(".codewhale").join("instructions.md"), |
| 23 | "shadowed\n", |
| 24 | )?; |
| 25 | fs::create_dir_all(workspace.join(".codewhale").join("rules"))?; |
| 26 | fs::write( |
| 27 | workspace.join(".codewhale").join("rules").join("style.md"), |
| 28 | "keep it small\n", |
| 29 | )?; |
| 30 | |
| 31 | let Some((addr, _runtime_threads, handle)) = |
| 32 | spawn_test_server_with_root_token_mobile_workspace( |
| 33 | root, |
| 34 | sessions_dir, |
| 35 | None, |
| 36 | false, |
| 37 | workspace.clone(), |
| 38 | ) |
| 39 | .await? |
| 40 | else { |
| 41 | return Ok(()); |
| 42 | }; |
| 43 | let client = crate::tls::reqwest_client(); |
| 44 | |
| 45 | let body: serde_json::Value = client |
| 46 | .get(format!("http://{addr}/v1/workspace/instructions")) |
| 47 | .send() |
| 48 | .await? |
| 49 | .error_for_status()? |
| 50 | .json() |
| 51 | .await?; |
| 52 | let sources = body["sources"].as_array().expect("sources array"); |
| 53 | |
| 54 | let agents = sources |
| 55 | .iter() |
| 56 | .find(|source| { |
| 57 | source["path"] |
| 58 | .as_str() |
| 59 | .is_some_and(|p| p.ends_with("AGENTS.md")) |
| 60 | }) |
| 61 | .expect("AGENTS.md row"); |
| 62 | assert_eq!(agents["kind"], "project"); |
| 63 | assert_eq!(agents["status"], "loaded"); |
| 64 | assert_eq!(agents["relative_path"], "AGENTS.md"); |
| 65 | assert_eq!(agents["exists"], true); |
| 66 | assert!(agents["bytes"].as_u64().is_some()); |
| 67 | |
| 68 | let shadowed = sources |
| 69 | .iter() |
| 70 | .find(|source| { |
| 71 | source["path"] |
| 72 | .as_str() |
| 73 | // `path` is the native absolute path, so it is backslashed on |
| 74 | // Windows; compare separator-agnostically rather than against |
| 75 | // one platform's spelling. |
| 76 | .is_some_and(|p| p.replace('\\', "/").ends_with(".codewhale/instructions.md")) |
| 77 | }) |
| 78 | .expect("shadowed workspace instructions row"); |
| 79 | assert_eq!(shadowed["status"], "shadowed"); |
| 80 | |
| 81 | let rule = sources |
| 82 | .iter() |
| 83 | .find(|source| source["kind"] == "rule") |
| 84 | .expect("rules file row"); |
| 85 | assert_eq!(rule["status"], "loaded"); |
| 86 | assert_eq!(rule["relative_path"], ".codewhale/rules/style.md"); |
| 87 | |
| 88 | // Unchecked-but-real candidates are reported too, so clients can render |
| 89 | // the full precedence map instead of a post-hoc file scan. |
| 90 | assert!( |
| 91 | sources.iter().any(|source| source["status"] == "missing"), |
| 92 | "missing candidates must be listed: {sources:?}" |
| 93 | ); |
| 94 | |
| 95 | assert_eq!(body["generated_fallback"], false); |
| 96 | assert!(body["aggregate_budget_bytes"].as_u64().unwrap() > 0); |
| 97 | |
| 98 | handle.abort(); |
| 99 | Ok(()) |
| 100 | } |
| 101 |