use super::*;
use tandem_memory::types::GlobalMemoryRecord;
use tokio::net::TcpListener;
async fn spawn_fake_github_mcp_server() -> (String, tokio::task::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind fake github mcp listener");
let addr = listener.local_addr().expect("fake github mcp addr");
let app = axum::Router::new().route(
"/",
axum::routing::post(|axum::Json(request): axum::Json<Value>| async move {
let id = request.get("id").cloned().unwrap_or(Value::Null);
let method = request
.get("method")
.and_then(Value::as_str)
.unwrap_or_default();
let result = match method {
"initialize" => json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"serverInfo": {
"name": "github",
"version": "test"
}
}),
"tools/list" => json!({
"tools": [
{
"name": "list_repository_issues",
"description": "List repository issues",
"inputSchema": {"type":"object"}
},
{
"name": "get_issue",
"description": "Get a GitHub issue",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.list_pull_requests",
"description": "List repository pull requests",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.get_pull_request",
"description": "Get a GitHub pull request",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.create_pull_request",
"description": "Create a GitHub pull request",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.merge_pull_request",
"description": "Merge a GitHub pull request",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.get_project",
"description": "Get a GitHub project",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.list_project_items",
"description": "List GitHub project items",
"inputSchema": {"type":"object"}
},
{
"name": "mcp.github.update_project_item_field",
"description": "Update a GitHub project item field",
"inputSchema": {"type":"object"}
}
]
}),
"tools/call" => {
let name = request
.get("params")
.and_then(|row| row.get("name"))
.and_then(Value::as_str)
.unwrap_or_default();
match name {
"mcp.github.create_pull_request" => json!({
"content": [
{
"type": "text",
"text": "created pull request #314"
}
],
"pull_request": {
"number": 314,
"title": "Guard startup recovery config loading.",
"state": "open",
"html_url": "https://github.com/user123/tandem/pull/314",
"head": {"ref": "coder/issue-313-fix"},
"base": {"ref": "main"}
}
}),
"mcp.github.merge_pull_request" => json!({
"content": [
{
"type": "text",
"text": "merged pull request #314"
}
],
"merged": true,
"sha": "abc123def456",
"message": "Pull request successfully merged",
"pull_request": {
"number": 314,
"state": "merged",
"html_url": "https://github.com/user123/tandem/pull/314"
}
}),
"mcp.github.get_project" => json!({
"id": "proj_42",
"owner": "user123",
"number": 42,
"title": "Coder Intake",
"fields": [
{
"id": "status_field_1",
"name": "Status",
"options": [
{"id": "opt_todo", "name": "TODO"},
{"id": "opt_progress", "name": "In Progress"},
{"id": "opt_review", "name": "In Review"},
{"id": "opt_blocked", "name": "Blocked"},
{"id": "opt_done", "name": "Done"}
]
}
]
}),
"mcp.github.list_project_items" => json!({
"items": [
{
"id": "PVT_item_1",
"title": "Guard startup recovery config loading",
"status": {"id": "opt_todo", "name": "TODO"},
"content": {
"type": "Issue",
"number": 313,
"title": "Guard startup recovery config loading",
"url": "https://github.com/user123/tandem/issues/313"
}
},
{
"id": "PVT_item_2",
"title": "Draft note",
"status": {"id": "opt_todo", "name": "TODO"},
"content": {
"type": "DraftIssue",
"title": "Draft note"
}
}
]
}),
"mcp.github.update_project_item_field" => json!({
"ok": true,
"item_id": request
.get("params")
.and_then(|row| row.get("arguments"))
.and_then(|row| row.get("project_item_id"))
.cloned()
.unwrap_or(Value::Null)
}),
_ => json!({
"content": [
{
"type": "text",
"text": format!("handled {name}")
}
]
}),
}
}
other => json!({
"content": [
{
"type": "text",
"text": format!("unsupported method {other}")
}
]
}),
};
axum::Json(json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
}))
}),
);
let server = tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("serve fake github mcp");
});
(format!("http://{addr}"), server)
}
fn init_coder_git_repo() -> std::path::PathBuf {
let repo_root =
std::env::temp_dir().join(format!("tandem-coder-worktree-test-{}", Uuid::new_v4()));
std::fs::create_dir_all(&repo_root).expect("create repo dir");
let status = std::process::Command::new("git")
.args(["init"])
.current_dir(&repo_root)
.status()
.expect("git init");
assert!(status.success());
let status = std::process::Command::new("git")
.args(["config", "user.email", "tests@tandem.local"])
.current_dir(&repo_root)
.status()
.expect("git config email");
assert!(status.success());
let status = std::process::Command::new("git")
.args(["config", "user.name", "Tandem Tests"])
.current_dir(&repo_root)
.status()
.expect("git config name");
assert!(status.success());
std::fs::write(repo_root.join("README.md"), "# coder test\n").expect("seed readme");
let status = std::process::Command::new("git")
.args(["add", "README.md"])
.current_dir(&repo_root)
.status()
.expect("git add");
assert!(status.success());
let status = std::process::Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(&repo_root)
.status()
.expect("git commit");
assert!(status.success());
repo_root
}
async fn create_coder_run_for_replay(app: axum::Router, body: Value) -> (Value, String) {
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_body = to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body");
let create_payload: Value = serde_json::from_slice(&create_body).expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
(create_payload, linked_context_run_id)
}
async fn checkpoint_and_replay_coder_run(app: axum::Router, linked_context_run_id: &str) -> Value {
let checkpoint_req = Request::builder()
.method("POST")
.uri(format!("/context/runs/{linked_context_run_id}/checkpoints"))
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "coder_replay_regression"
})
.to_string(),
))
.expect("checkpoint request");
let checkpoint_resp = app
.clone()
.oneshot(checkpoint_req)
.await
.expect("checkpoint response");
assert_eq!(checkpoint_resp.status(), StatusCode::OK);
let checkpoint_body = to_bytes(checkpoint_resp.into_body(), usize::MAX)
.await
.expect("checkpoint body");
let checkpoint_payload: Value =
serde_json::from_slice(&checkpoint_body).expect("checkpoint json");
assert_eq!(
checkpoint_payload
.get("checkpoint")
.and_then(|row| row.get("run_id"))
.and_then(Value::as_str),
Some(linked_context_run_id)
);
let replay_req = Request::builder()
.method("GET")
.uri(format!("/context/runs/{linked_context_run_id}/replay"))
.body(Body::empty())
.expect("replay request");
let replay_resp = app
.clone()
.oneshot(replay_req)
.await
.expect("replay response");
assert_eq!(replay_resp.status(), StatusCode::OK);
let replay_body = to_bytes(replay_resp.into_body(), usize::MAX)
.await
.expect("replay body");
serde_json::from_slice(&replay_body).expect("replay json")
}
#[tokio::test]
async fn coder_issue_triage_run_create_get_and_list() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-1",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "issue",
"number": 1234,
"url": "https://github.com/user123/tandem/issues/1234"
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_body = to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body");
let create_payload: Value = serde_json::from_slice(&create_body).expect("create json");
assert_eq!(
create_payload
.get("coder_run")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_triage")
);
assert_eq!(
create_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("repo_inspection")
);
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let get_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-1")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_body = to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body");
let get_payload: Value = serde_json::from_slice(&get_body).expect("get json");
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_issue_triage")
);
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(5)
);
let tasks = get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.cloned()
.expect("tasks");
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("ingest_reference"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("done")
);
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("retrieve_memory"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("done")
);
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str) == Some("inspect_repo"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("runnable")
);
assert!(get_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
}))
.unwrap_or(false));
assert_eq!(
get_payload
.get("memory_hits")
.and_then(|row| row.get("query"))
.and_then(Value::as_str),
Some("user123/tandem issue #1234")
);
assert_eq!(
get_payload
.get("memory_candidates")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
let list_req = Request::builder()
.method("GET")
.uri("/coder/runs?workflow_mode=issue_triage")
.body(Body::empty())
.expect("list request");
let list_resp = app.clone().oneshot(list_req).await.expect("list response");
assert_eq!(list_resp.status(), StatusCode::OK);
let list_body = to_bytes(list_resp.into_body(), usize::MAX)
.await
.expect("list body");
let list_payload: Value = serde_json::from_slice(&list_body).expect("list json");
assert_eq!(
list_payload
.get("runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
list_payload
.get("runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str),
Some(linked_context_run_id.as_str())
);
}
#[tokio::test]
async fn coder_pr_review_run_create_gets_seeded_review_tasks() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-1",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 88,
"url": "https://github.com/user123/tandem/pull/88"
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_body = to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body");
let create_payload: Value = serde_json::from_slice(&create_body).expect("create json");
assert_eq!(
create_payload
.get("coder_run")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let get_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-1")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_body = to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body");
let get_payload: Value = serde_json::from_slice(&get_body).expect("get json");
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_pr_review")
);
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
let tasks = get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.cloned()
.expect("tasks");
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("retrieve_memory"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("done")
);
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("inspect_pull_request"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("runnable")
);
assert!(get_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
}))
.unwrap_or(false));
assert!(get_payload
.get("coder_artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
&& row.get("exists").and_then(Value::as_bool) == Some(true)
&& row.get("payload_format").and_then(Value::as_str) == Some("json")
&& row.get("payload").is_some()
}))
.unwrap_or(false));
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.len())
.filter(|count| *count >= 3),
Some(
get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.len())
.unwrap_or_default()
)
);
assert!(get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("inspect_pull_request")
}))
.unwrap_or(false));
assert!(get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("review_pull_request")
}))
.unwrap_or(false));
assert_eq!(
get_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("repo_inspection")
);
assert_eq!(
get_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str),
Some(linked_context_run_id.as_str())
);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-1/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload.get("query").and_then(Value::as_str),
Some("user123/tandem pull request #88 review regressions blockers requested changes")
);
}
#[tokio::test]
async fn coder_issue_fix_run_create_gets_seeded_fix_tasks() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-1",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 77
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let get_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-fix-1")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_payload: Value = serde_json::from_slice(
&to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body"),
)
.expect("get json");
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_issue_fix")
);
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
let tasks = get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.cloned()
.expect("tasks");
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("retrieve_memory"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("done")
);
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("inspect_issue_context"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("runnable")
);
assert!(get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("prepare_fix")
}))
.unwrap_or(false));
assert!(get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("validate_fix")
}))
.unwrap_or(false));
assert!(get_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
}))
.unwrap_or(false));
assert_eq!(
get_payload
.get("memory_hits")
.and_then(|row| row.get("query"))
.and_then(Value::as_str),
Some("user123/tandem issue #77")
);
}
#[tokio::test]
async fn coder_issue_fix_validation_report_advances_fix_run() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-validate",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 79
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let validation_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-validate/issue-fix-validation-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Added a guard around the startup recovery path.",
"root_cause": "Startup recovery skipped the config fallback branch.",
"fix_strategy": "guard fallback branch",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_steps": ["cargo test -p tandem-server coder_issue_fix_validation_report_advances_fix_run -- --test-threads=1"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "targeted validation regression passed"
}],
"memory_hits_used": ["memory-hit-fix-validation-1"]
})
.to_string(),
))
.expect("validation request");
let validation_resp = app
.clone()
.oneshot(validation_req)
.await
.expect("validation response");
assert_eq!(validation_resp.status(), StatusCode::OK);
let validation_payload: Value = serde_json::from_slice(
&to_bytes(validation_resp.into_body(), usize::MAX)
.await
.expect("validation body"),
)
.expect("validation json");
assert_eq!(
validation_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_validation_report")
);
assert_eq!(
validation_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("validation_memory")
})),
Some(true)
);
assert_eq!(
validation_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
assert_eq!(
validation_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("artifact_write")
);
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Running);
for workflow_node_id in [
"inspect_issue_context",
"retrieve_memory",
"prepare_fix",
"validate_fix",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("write_fix_artifact"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Runnable)
);
}
#[tokio::test]
async fn coder_issue_fix_failed_validation_writes_regression_signal() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-validation-failed",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 80
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let validation_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-validation-failed/issue-fix-validation-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Guarded the startup recovery path, but one regression still failed.",
"root_cause": "Startup recovery skipped the config fallback branch.",
"fix_strategy": "guard fallback branch",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_steps": ["cargo test -p tandem-server coder_issue_fix_failed_validation_writes_regression_signal -- --test-threads=1"],
"validation_results": [{
"kind": "test",
"status": "failed",
"summary": "targeted startup recovery regression still fails"
}],
"memory_hits_used": ["memory-hit-fix-validation-failure-1"]
})
.to_string(),
))
.expect("validation request");
let validation_resp = app
.clone()
.oneshot(validation_req)
.await
.expect("validation response");
assert_eq!(validation_resp.status(), StatusCode::OK);
let validation_payload: Value = serde_json::from_slice(
&to_bytes(validation_resp.into_body(), usize::MAX)
.await
.expect("validation body"),
)
.expect("validation json");
assert_eq!(
validation_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("regression_signal")
})),
Some(true)
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-fix-validation-failed/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let regression_signal = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("regression_signal"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("regression signal payload");
assert_eq!(
regression_signal
.get("regression_signals")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("failed")
);
assert_eq!(
regression_signal
.get("validation_artifact_path")
.and_then(Value::as_str)
.is_some(),
true
);
}
#[tokio::test]
async fn coder_issue_fix_worker_failure_writes_run_outcome() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-worker-failure",
"workflow_mode": "issue_fix",
"model_provider": "missing-provider",
"model_id": "missing-model",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 81
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let first_step_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-worker-failure/execute-next")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("first step request");
let first_step_resp = app
.clone()
.oneshot(first_step_req)
.await
.expect("first step response");
assert_eq!(first_step_resp.status(), StatusCode::OK);
let second_step_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-worker-failure/execute-next")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("second step request");
let second_step_resp = app
.clone()
.oneshot(second_step_req)
.await
.expect("second step response");
assert_eq!(second_step_resp.status(), StatusCode::OK);
let second_step_payload: Value = serde_json::from_slice(
&to_bytes(second_step_resp.into_body(), usize::MAX)
.await
.expect("second step body"),
)
.expect("second step json");
assert_eq!(
second_step_payload
.get("dispatch_result")
.and_then(|row| row.get("code"))
.and_then(Value::as_str),
Some("CODER_WORKER_SESSION_FAILED")
);
assert_eq!(
second_step_payload
.get("dispatch_result")
.and_then(|row| row.get("generated_candidates"))
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("run_outcome")
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-fix-worker-failure/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let run_outcome_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("run_outcome"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("run outcome payload");
assert_eq!(
run_outcome_payload.get("result").and_then(Value::as_str),
Some("issue_fix_prepare_failed")
);
assert_eq!(
run_outcome_payload
.get("worker_artifact_type")
.and_then(Value::as_str),
Some("coder_issue_fix_worker_session")
);
assert_eq!(
run_outcome_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
.map(|value| value.starts_with("session-")),
Some(true)
);
assert_eq!(
run_outcome_payload
.get("worker_run_reference")
.and_then(Value::as_str),
run_outcome_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
);
}
#[tokio::test]
async fn coder_pr_review_worker_failure_writes_run_outcome() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-worker-failure",
"workflow_mode": "pr_review",
"model_provider": "missing-provider",
"model_id": "missing-model",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 82
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let first_step_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-worker-failure/execute-next")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("first step request");
let first_step_resp = app
.clone()
.oneshot(first_step_req)
.await
.expect("first step response");
assert_eq!(first_step_resp.status(), StatusCode::OK);
let second_step_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-worker-failure/execute-next")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("second step request");
let second_step_resp = app
.clone()
.oneshot(second_step_req)
.await
.expect("second step response");
assert_eq!(second_step_resp.status(), StatusCode::OK);
let second_step_payload: Value = serde_json::from_slice(
&to_bytes(second_step_resp.into_body(), usize::MAX)
.await
.expect("second step body"),
)
.expect("second step json");
assert_eq!(
second_step_payload
.get("dispatch_result")
.and_then(|row| row.get("code"))
.and_then(Value::as_str),
Some("CODER_WORKER_SESSION_FAILED")
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-worker-failure/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let run_outcome_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("run_outcome"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("run outcome payload");
assert_eq!(
run_outcome_payload.get("result").and_then(Value::as_str),
Some("pr_review_failed")
);
assert_eq!(
run_outcome_payload
.get("worker_artifact_type")
.and_then(Value::as_str),
Some("coder_pr_review_worker_session")
);
assert_eq!(
run_outcome_payload
.get("worker_run_reference")
.and_then(Value::as_str),
run_outcome_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
.or_else(|| {
run_outcome_payload
.get("worker_session_id")
.and_then(Value::as_str)
})
);
}
#[tokio::test]
async fn coder_issue_fix_execute_next_drives_task_runtime_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-execute-next",
"workflow_mode": "issue_fix",
"model_provider": "local",
"model_id": "echo-1",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 199
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let mut changed_file_artifact_path: Option<String> = None;
for expected in [
"inspect_issue_context",
"prepare_fix",
"validate_fix",
"write_fix_artifact",
] {
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-execute-next/execute-next")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test"
})
.to_string(),
))
.expect("execute request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute body"),
)
.expect("execute json");
assert_eq!(
execute_payload
.get("task")
.and_then(|row| row.get("workflow_node_id"))
.and_then(Value::as_str),
Some(expected)
);
if expected == "prepare_fix" {
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_artifact"))
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_issue_fix_worker_session")
);
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("plan_artifact"))
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_issue_fix_plan")
);
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.and_then(|row| row.get("worker_run_reference"))
.and_then(Value::as_str),
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.and_then(|row| row.get("session_context_run_id"))
.and_then(Value::as_str)
);
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.and_then(|row| row.get("model"))
.and_then(|row| row.get("provider_id"))
.and_then(Value::as_str),
Some("local")
);
assert!(execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.and_then(|row| row.get("assistant_text"))
.and_then(Value::as_str)
.is_some_and(|text| text.contains("Echo:")));
let changed_file_entries = execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.and_then(|row| row.get("changed_file_entries"))
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
if !changed_file_entries.is_empty() {
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("changed_file_artifact"))
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_changed_file_evidence")
);
changed_file_artifact_path = execute_payload
.get("dispatch_result")
.and_then(|row| row.get("changed_file_artifact"))
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.map(ToString::to_string);
}
} else if expected == "validate_fix" {
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("artifact"))
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_validation_report")
);
}
}
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
for workflow_node_id in [
"inspect_issue_context",
"retrieve_memory",
"prepare_fix",
"validate_fix",
"write_fix_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
let blackboard = load_context_blackboard(&state, &linked_context_run_id);
assert!(blackboard
.artifacts
.iter()
.any(|artifact| { artifact.artifact_type == "coder_issue_fix_worker_session" }));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| { artifact.artifact_type == "coder_issue_fix_plan" }));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| { artifact.artifact_type == "coder_issue_fix_validation_session" }));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| { artifact.artifact_type == "coder_patch_summary" }));
if let Some(changed_file_artifact_path) = changed_file_artifact_path {
let changed_file_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(&changed_file_artifact_path)
.await
.expect("read changed file artifact"),
)
.expect("parse changed file artifact");
assert_eq!(
changed_file_payload
.get("worker_run_reference")
.and_then(Value::as_str),
changed_file_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
.or_else(|| {
changed_file_payload
.get("worker_session_id")
.and_then(Value::as_str)
})
);
assert!(changed_file_payload
.get("entries")
.and_then(Value::as_array)
.is_some_and(|rows| rows.iter().any(|row| {
row.get("path").and_then(Value::as_str)
== Some("crates/tandem-server/src/http/coder.rs")
&& row
.get("preview")
.and_then(Value::as_str)
.is_some_and(|preview| preview.contains("Summary:"))
})));
let patch_summary_path = blackboard
.artifacts
.iter()
.find(|artifact| artifact.artifact_type == "coder_patch_summary")
.map(|artifact| artifact.path.clone())
.expect("patch summary path");
let patch_summary_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(&patch_summary_path)
.await
.expect("read patch summary artifact"),
)
.expect("parse patch summary artifact");
assert_eq!(
patch_summary_payload
.get("worker_run_reference")
.and_then(Value::as_str),
patch_summary_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
.or_else(|| {
patch_summary_payload
.get("worker_session_id")
.and_then(Value::as_str)
})
);
assert_eq!(
patch_summary_payload
.get("validation_run_reference")
.and_then(Value::as_str),
patch_summary_payload
.get("validation_session_context_run_id")
.and_then(Value::as_str)
.or_else(|| {
patch_summary_payload
.get("validation_session_id")
.and_then(Value::as_str)
})
);
assert!(patch_summary_payload
.get("changed_file_entries")
.and_then(Value::as_array)
.is_some_and(|rows| rows.iter().any(|row| {
row.get("path").and_then(Value::as_str)
== Some("crates/tandem-server/src/http/coder.rs")
})));
}
}
#[tokio::test]
async fn coder_issue_fix_worker_uses_managed_worktree_for_git_repo() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let repo_root = init_coder_git_repo();
let nested_workspace_root = repo_root.join("nested").join("workspace");
std::fs::create_dir_all(&nested_workspace_root).expect("create nested workspace");
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-managed-worktree",
"workflow_mode": "issue_fix",
"model_provider": "local",
"model_id": "echo-1",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": nested_workspace_root.to_string_lossy(),
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 200
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let mut saw_prepare_fix = false;
for _ in 0..2 {
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-managed-worktree/execute-next")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test"
})
.to_string(),
))
.expect("execute request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute body"),
)
.expect("execute json");
if execute_payload
.get("task")
.and_then(|row| row.get("workflow_node_id"))
.and_then(Value::as_str)
== Some("prepare_fix")
{
saw_prepare_fix = true;
let worker_session = execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session"))
.cloned()
.expect("worker session");
let worker_workspace_root = worker_session
.get("worker_workspace_root")
.and_then(Value::as_str)
.expect("worker workspace root")
.to_string();
assert!(worker_workspace_root.contains("/.tandem/worktrees/"));
assert_eq!(
worker_session
.get("worker_workspace_repo_root")
.and_then(Value::as_str),
Some(repo_root.to_string_lossy().as_ref())
);
assert_eq!(
worker_session.get("task_id").and_then(Value::as_str),
execute_payload
.get("task")
.and_then(|row| row.get("id"))
.and_then(Value::as_str)
);
assert!(!std::path::Path::new(&worker_workspace_root).exists());
let managed_root = repo_root.join(".tandem").join("worktrees");
if managed_root.exists() {
let entries = std::fs::read_dir(&managed_root)
.expect("list managed root")
.filter_map(Result::ok)
.collect::<Vec<_>>();
assert!(entries.is_empty());
}
break;
}
}
assert!(saw_prepare_fix, "expected prepare_fix task to run");
let _ = std::fs::remove_dir_all(repo_root);
}
#[tokio::test]
async fn coder_issue_fix_execute_all_runs_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-execute-all",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 299
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-execute-all/execute-all")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test",
"max_steps": 8
})
.to_string(),
))
.expect("execute-all request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute-all response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute-all body"),
)
.expect("execute-all json");
assert_eq!(
execute_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
assert_eq!(
execute_payload
.get("stopped_reason")
.and_then(Value::as_str),
Some("run_completed")
);
assert!(execute_payload
.get("executed_steps")
.and_then(Value::as_u64)
.is_some_and(|count| count >= 4));
}
#[tokio::test]
async fn coder_pr_review_execute_all_runs_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-execute-all",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 300
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-execute-all/execute-all")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test",
"max_steps": 8
})
.to_string(),
))
.expect("execute-all request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute-all response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute-all body"),
)
.expect("execute-all json");
assert_eq!(
execute_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
assert_eq!(
execute_payload
.get("stopped_reason")
.and_then(Value::as_str),
Some("run_completed")
);
assert!(execute_payload
.get("executed_steps")
.and_then(Value::as_u64)
.is_some_and(|count| count >= 3));
}
#[tokio::test]
async fn coder_merge_recommendation_execute_all_runs_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-execute-all",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 301
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-execute-all/execute-all")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test",
"max_steps": 8
})
.to_string(),
))
.expect("execute-all request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute-all response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute-all body"),
)
.expect("execute-all json");
assert_eq!(
execute_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
assert_eq!(
execute_payload
.get("stopped_reason")
.and_then(Value::as_str),
Some("run_completed")
);
assert!(execute_payload
.get("executed_steps")
.and_then(Value::as_u64)
.is_some_and(|count| count >= 3));
}
#[tokio::test]
async fn coder_issue_fix_summary_create_writes_artifact() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-summary",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 78
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-summary/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Guard the missing config branch and add a regression test for startup recovery.",
"root_cause": "Nil config fallback was skipped during startup recovery.",
"fix_strategy": "add startup fallback guard",
"changed_files": [
"crates/tandem-server/src/http/coder.rs",
"crates/tandem-server/src/http/tests/coder.rs"
],
"validation_steps": ["cargo test -p tandem-server coder_issue_fix_summary_create_writes_artifact -- --test-threads=1"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "targeted coder issue-fix regression passed"
}],
"memory_hits_used": ["memory-hit-fix-1"],
"notes": "Prior triage memory pointed to startup recovery flow."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_issue_fix_summary")
);
assert_eq!(
summary_payload
.get("validation_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_validation_report")
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("fix_pattern") })),
Some(true)
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("validation_memory")
})),
Some(true)
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("run_outcome") })),
Some(true)
);
assert_eq!(
summary_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
for workflow_node_id in [
"inspect_issue_context",
"retrieve_memory",
"prepare_fix",
"validate_fix",
"write_fix_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
let artifacts_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-fix-summary/artifacts")
.body(Body::empty())
.expect("artifacts request");
let artifacts_resp = app
.clone()
.oneshot(artifacts_req)
.await
.expect("artifacts response");
assert_eq!(artifacts_resp.status(), StatusCode::OK);
let artifacts_payload: Value = serde_json::from_slice(
&to_bytes(artifacts_resp.into_body(), usize::MAX)
.await
.expect("artifacts body"),
)
.expect("artifacts json");
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_issue_fix_summary")
}))
.unwrap_or(false));
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_validation_report")
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_issue_fix_pr_draft_create_writes_artifact() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-pr-draft",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 312
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-draft/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Guard startup recovery config loading.",
"root_cause": "Recovery skipped the nil-config fallback branch.",
"fix_strategy": "restore the fallback guard and add a regression test",
"changed_files": [
"crates/tandem-server/src/http/coder.rs",
"crates/tandem-server/src/http/tests/coder.rs"
],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "targeted issue-fix regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-draft/pr-draft")
.header("content-type", "application/json")
.body(Body::from(
json!({
"base_branch": "main"
})
.to_string(),
))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let draft_payload: Value = serde_json::from_slice(
&to_bytes(draft_resp.into_body(), usize::MAX)
.await
.expect("draft body"),
)
.expect("draft json");
assert_eq!(
draft_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_pr_draft")
);
assert_eq!(
draft_payload
.get("approval_required")
.and_then(Value::as_bool),
Some(true)
);
let artifact_path = draft_payload
.get("artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.expect("draft artifact path");
let artifact_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(artifact_path)
.await
.expect("read draft artifact"),
)
.expect("parse draft artifact");
assert_eq!(
artifact_payload.get("title").and_then(Value::as_str),
Some("Guard startup recovery config loading.")
);
assert!(artifact_payload
.get("body")
.and_then(Value::as_str)
.is_some_and(|body| body.contains("Closes #312")));
assert!(artifact_payload
.get("body")
.and_then(Value::as_str)
.is_some_and(|body| body.contains("coder.rs")));
}
#[tokio::test]
async fn coder_issue_fix_pr_submit_dry_run_writes_submission_artifact() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-pr-submit",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 313
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add missing fallback to startup recovery.",
"root_cause": "Recovery skipped the nil-config guard.",
"fix_strategy": "restore startup fallback and add a targeted regression",
"changed_files": [
"crates/tandem-server/src/http/coder.rs"
],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Looks good for a draft PR",
"dry_run": true
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_pr_submission")
);
assert_eq!(
submit_payload
.get("external_action")
.and_then(|row| row.get("capability_id"))
.and_then(Value::as_str),
Some("github.create_pull_request")
);
assert_eq!(
submit_payload
.get("external_action")
.and_then(|row| row.get("source_kind"))
.and_then(Value::as_str),
Some("coder")
);
assert!(submit_payload
.get("duplicate_linkage_candidate")
.is_some_and(Value::is_null));
assert_eq!(
submit_payload.get("submitted").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload.get("dry_run").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
submit_payload
.get("worker_run_reference")
.and_then(Value::as_str),
submit_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
);
assert_eq!(
submit_payload
.get("validation_run_reference")
.and_then(Value::as_str),
submit_payload
.get("validation_session_context_run_id")
.and_then(Value::as_str)
);
assert!(submit_payload
.get("submitted_github_ref")
.is_some_and(Value::is_null));
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
assert_eq!(
submit_payload
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
let submission_artifact_payload = submit_payload
.get("artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|body| serde_json::from_str::<Value>(&body).ok())
.expect("submission artifact payload");
assert_eq!(
submission_artifact_payload
.get("worker_run_reference")
.and_then(Value::as_str),
submission_artifact_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
);
assert_eq!(
submission_artifact_payload
.get("validation_run_reference")
.and_then(Value::as_str),
submission_artifact_payload
.get("validation_session_context_run_id")
.and_then(Value::as_str)
);
assert_eq!(
submission_artifact_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
}
#[tokio::test]
async fn coder_issue_fix_pr_submit_real_submit_writes_canonical_pr_identity() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let mut rx = state.event_bus.subscribe();
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-pr-submit-real",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 313
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-real/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add missing fallback to startup recovery.",
"root_cause": "Recovery skipped the nil-config guard.",
"fix_strategy": "restore startup fallback and add a targeted regression",
"changed_files": [
"crates/tandem-server/src/http/coder.rs"
],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-real/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-real/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Ready to open the draft PR",
"dry_run": false,
"mcp_server": "github",
"spawn_follow_on_runs": ["pr_review"]
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("submitted").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
submit_payload
.get("submitted_github_ref")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("pull_request")
);
assert_eq!(
submit_payload
.get("pull_request")
.and_then(|row| row.get("number"))
.and_then(Value::as_u64),
Some(314)
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(2)
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("parent_coder_run_id"))
.and_then(Value::as_str),
Some("coder-issue-fix-pr-submit-real")
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("spawn_mode"))
.and_then(Value::as_str),
Some("template")
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("required_completed_workflow_modes"))
.and_then(Value::as_array)
.map(|rows| rows.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
Some(vec!["pr_review"])
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("execution_policy_preview"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("merge_submit_policy_preview"))
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("merge_submit_policy_preview"))
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("project_auto_merge_policy_disabled")
);
assert_eq!(
submit_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("merge_submit_policy_preview"))
.and_then(|row| row.get("manual"))
.and_then(|row| row.get("policy"))
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_merge_execution_request")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("parent_coder_run_id"))
.and_then(Value::as_str),
Some("coder-issue-fix-pr-submit-real")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("origin"))
.and_then(Value::as_str),
Some("issue_fix_pr_submit_auto")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("spawn_mode"))
.and_then(Value::as_str),
Some("auto")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("execution_policy"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_pr_submission")
);
let artifact_path = submit_payload
.get("artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.expect("submit artifact path");
let artifact_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(artifact_path)
.await
.expect("read submit artifact"),
)
.expect("parse submit artifact");
assert_eq!(
artifact_payload
.get("submitted_github_ref")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("pull_request")
);
assert_eq!(
artifact_payload
.get("submitted_github_ref")
.and_then(|row| row.get("number"))
.and_then(Value::as_u64),
Some(314)
);
assert_eq!(
artifact_payload
.get("pull_request")
.and_then(|row| row.get("number"))
.and_then(Value::as_u64),
Some(314)
);
assert_eq!(
artifact_payload.get("owner").and_then(Value::as_str),
Some("user123")
);
assert_eq!(
artifact_payload.get("repo").and_then(Value::as_str),
Some("tandem")
);
assert_eq!(
artifact_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(2)
);
assert_eq!(
artifact_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("spawn_mode"))
.and_then(Value::as_str),
Some("template")
);
assert_eq!(
artifact_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
artifact_payload
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
assert_eq!(
artifact_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
assert_eq!(
artifact_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("execution_policy_preview"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
artifact_payload
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("merge_submit_policy_preview"))
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
artifact_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("execution_policy"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
artifact_payload
.get("duplicate_linkage_candidate")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("duplicate_linkage")
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-fix-pr-submit-real/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let duplicate_linkage = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("duplicate_linkage"))
})
.expect("duplicate linkage candidate");
assert_eq!(
duplicate_linkage
.get("payload")
.and_then(|row| row.get("linked_issue_numbers"))
.cloned(),
Some(json!([313]))
);
assert_eq!(
duplicate_linkage
.get("payload")
.and_then(|row| row.get("linked_pr_numbers"))
.cloned(),
Some(json!([314]))
);
let follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-real/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "pr_review",
"coder_run_id": "coder-follow-on-pr-review"
})
.to_string(),
))
.expect("follow-on request");
let follow_on_resp = app
.clone()
.oneshot(follow_on_req)
.await
.expect("follow-on response");
assert_eq!(follow_on_resp.status(), StatusCode::OK);
let follow_on_payload: Value = serde_json::from_slice(
&to_bytes(follow_on_resp.into_body(), usize::MAX)
.await
.expect("follow-on body"),
)
.expect("follow-on json");
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("github_ref"))
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("pull_request")
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("github_ref"))
.and_then(|row| row.get("number"))
.and_then(Value::as_u64),
Some(314)
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("parent_coder_run_id"))
.and_then(Value::as_str),
Some("coder-issue-fix-pr-submit-real")
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("origin"))
.and_then(Value::as_str),
Some("issue_fix_pr_submit_manual_follow_on")
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("origin_artifact_type"))
.and_then(Value::as_str),
Some("coder_pr_submission")
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("spawn_mode"))
.and_then(Value::as_str),
Some("manual")
);
assert_eq!(
follow_on_payload
.get("coder_run")
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("required_completed_workflow_modes"))
.and_then(Value::as_array)
.map(|rows| rows.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
Some(Vec::<&str>::new())
);
assert_eq!(
follow_on_payload
.get("execution_policy")
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
follow_on_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("duplicate_linkage")
);
let follow_on_candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-follow-on-pr-review/memory-candidates")
.body(Body::empty())
.expect("follow-on candidates request");
let follow_on_candidates_resp = app
.clone()
.oneshot(follow_on_candidates_req)
.await
.expect("follow-on candidates response");
assert_eq!(follow_on_candidates_resp.status(), StatusCode::OK);
let follow_on_candidates_payload: Value = serde_json::from_slice(
&to_bytes(follow_on_candidates_resp.into_body(), usize::MAX)
.await
.expect("follow-on candidates body"),
)
.expect("follow-on candidates json");
let follow_on_duplicate_linkage = follow_on_candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("duplicate_linkage"))
})
.expect("follow-on duplicate linkage candidate");
assert_eq!(
follow_on_duplicate_linkage
.get("payload")
.and_then(|row| row.get("linked_issue_numbers"))
.cloned(),
Some(json!([313]))
);
assert_eq!(
follow_on_duplicate_linkage
.get("payload")
.and_then(|row| row.get("linked_pr_numbers"))
.cloned(),
Some(json!([314]))
);
let review_hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-follow-on-pr-review/memory-hits")
.body(Body::empty())
.expect("review hits request");
let review_hits_resp = app
.clone()
.oneshot(review_hits_req)
.await
.expect("review hits response");
assert_eq!(review_hits_resp.status(), StatusCode::OK);
let review_hits_payload: Value = serde_json::from_slice(
&to_bytes(review_hits_resp.into_body(), usize::MAX)
.await
.expect("review hits body"),
)
.expect("review hits json");
let review_duplicate_linkage = review_hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("duplicate_linkage"))
})
.expect("review duplicate linkage hit");
assert_eq!(
review_duplicate_linkage
.get("same_linked_pr")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
review_hits_payload
.get("retrieval_policy")
.and_then(|row| row.get("prioritized_kinds"))
.cloned(),
Some(json!([
"review_memory",
"merge_recommendation_memory",
"duplicate_linkage",
"regression_signal",
"run_outcome"
]))
);
let submitted_event = next_event_of_type(&mut rx, "coder.pr.submitted").await;
assert_eq!(
submitted_event
.properties
.get("submitted_github_ref")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("pull_request")
);
assert_eq!(
submitted_event
.properties
.get("pull_request_number")
.and_then(Value::as_u64),
Some(314)
);
assert_eq!(
submitted_event
.properties
.get("follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(2)
);
assert_eq!(
submitted_event
.properties
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("execution_policy_preview"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
submitted_event
.properties
.get("follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.get(1))
.and_then(|row| row.get("merge_submit_policy_preview"))
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submitted_event
.properties
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
submitted_event
.properties
.get("duplicate_linkage_candidate")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("duplicate_linkage")
);
assert_eq!(
submitted_event
.properties
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("execution_policy"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submitted_event
.properties
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
}
#[tokio::test]
async fn coder_issue_fix_pr_submit_merge_auto_spawn_requires_opt_in() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let mut rx = state.event_bus.subscribe();
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-pr-submit-merge-policy",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 313
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-merge-policy/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add missing fallback to startup recovery.",
"root_cause": "Recovery skipped the nil-config guard.",
"fix_strategy": "restore startup fallback and add a targeted regression",
"changed_files": [
"crates/tandem-server/src/http/coder.rs"
],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-merge-policy/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-pr-submit-merge-policy/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Open the draft PR and queue review",
"dry_run": false,
"mcp_server": "github",
"spawn_follow_on_runs": ["merge_recommendation"]
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("origin"))
.and_then(Value::as_str),
Some("issue_fix_pr_submit_auto")
);
assert_eq!(
submit_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("merge_auto_spawn_opted_in"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
submit_payload
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("merge_recommendation")
);
assert_eq!(
submit_payload
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_explicit_auto_merge_recommendation_opt_in")
);
let submitted_event = next_event_of_type(&mut rx, "coder.pr.submitted").await;
assert_eq!(
submitted_event
.properties
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
submitted_event
.properties
.get("skipped_follow_on_runs")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
}
#[tokio::test]
async fn coder_merge_follow_on_execution_waits_for_completed_review() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let mut rx = state.event_bus.subscribe();
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-follow-on-policy-parent",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 313
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-policy-parent/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add missing fallback to startup recovery.",
"root_cause": "Recovery skipped the nil-config guard.",
"fix_strategy": "restore startup fallback and add a targeted regression",
"changed_files": [
"crates/tandem-server/src/http/coder.rs"
],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-policy-parent/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-policy-parent/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Open the draft PR",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
assert_eq!(submit_resp.status(), StatusCode::OK);
let merge_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-policy-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "merge_recommendation",
"coder_run_id": "coder-follow-on-merge"
})
.to_string(),
))
.expect("merge follow-on request");
let merge_follow_on_resp = app
.clone()
.oneshot(merge_follow_on_req)
.await
.expect("merge follow-on response");
assert_eq!(merge_follow_on_resp.status(), StatusCode::OK);
let merge_follow_on_payload: Value = serde_json::from_slice(
&to_bytes(merge_follow_on_resp.into_body(), usize::MAX)
.await
.expect("merge follow-on body"),
)
.expect("merge follow-on json");
assert_eq!(
merge_follow_on_payload
.get("execution_policy")
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
merge_follow_on_payload
.get("execution_policy")
.and_then(|row| row.get("policy"))
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_completed_pr_review_follow_on")
);
assert_eq!(
merge_follow_on_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
merge_follow_on_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("requires_merge_execution_request")
);
assert_eq!(
merge_follow_on_payload
.get("merge_submit_policy")
.and_then(|row| row.get("manual"))
.and_then(|row| row.get("policy"))
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_merge_execution_request")
);
let blocked_execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-merge/execute-next")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("blocked execute request");
let blocked_execute_resp = app
.clone()
.oneshot(blocked_execute_req)
.await
.expect("blocked execute response");
assert_eq!(blocked_execute_resp.status(), StatusCode::OK);
let blocked_execute_payload: Value = serde_json::from_slice(
&to_bytes(blocked_execute_resp.into_body(), usize::MAX)
.await
.expect("blocked execute body"),
)
.expect("blocked execute json");
assert_eq!(
blocked_execute_payload.get("code").and_then(Value::as_str),
Some("CODER_EXECUTION_POLICY_BLOCKED")
);
assert_eq!(
blocked_execute_payload
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_completed_pr_review_follow_on")
);
assert_eq!(
blocked_execute_payload
.get("execution_policy")
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
blocked_execute_payload
.get("coder_run")
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str),
Some("coder-follow-on-merge")
);
assert_eq!(
blocked_execute_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
let blocked_event = loop {
let event = next_event_of_type(&mut rx, "coder.run.phase_changed").await;
if event.properties.get("event_type").and_then(Value::as_str)
== Some("execution_policy_blocked")
{
break event;
}
};
assert_eq!(
blocked_event
.properties
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_completed_pr_review_follow_on")
);
assert_eq!(
blocked_event
.properties
.get("coder_run_id")
.and_then(Value::as_str),
Some("coder-follow-on-merge")
);
assert!(blocked_event
.properties
.get("linked_context_run_id")
.and_then(Value::as_str)
.is_some());
assert_eq!(
blocked_event
.properties
.get("workflow_mode")
.and_then(Value::as_str),
Some("merge_recommendation")
);
assert_eq!(
blocked_event
.properties
.get("repo_binding")
.and_then(|row| row.get("repo_slug"))
.and_then(Value::as_str),
Some("user123/tandem")
);
assert_eq!(
blocked_event
.properties
.get("github_ref")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("pull_request")
);
assert_eq!(
blocked_event
.properties
.get("phase")
.and_then(Value::as_str),
Some("policy_blocked")
);
let merge_run_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-follow-on-merge")
.body(Body::empty())
.expect("merge get request");
let merge_run_resp = app
.clone()
.oneshot(merge_run_req)
.await
.expect("merge get response");
assert_eq!(merge_run_resp.status(), StatusCode::OK);
let merge_run_payload: Value = serde_json::from_slice(
&to_bytes(merge_run_resp.into_body(), usize::MAX)
.await
.expect("merge get body"),
)
.expect("merge get json");
assert_eq!(
merge_run_payload
.get("coder_run")
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("required_completed_workflow_modes"))
.and_then(Value::as_array)
.map(|rows| rows.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
Some(vec!["pr_review"])
);
assert_eq!(
merge_run_payload
.get("execution_policy")
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
merge_run_payload
.get("execution_policy")
.and_then(|row| row.get("policy"))
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_completed_pr_review_follow_on")
);
let review_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-policy-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "pr_review",
"coder_run_id": "coder-follow-on-review"
})
.to_string(),
))
.expect("review follow-on request");
let review_follow_on_resp = app
.clone()
.oneshot(review_follow_on_req)
.await
.expect("review follow-on response");
assert_eq!(review_follow_on_resp.status(), StatusCode::OK);
let review_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-review/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "approve",
"summary": "Looks good after targeted review.",
"risk_level": "low",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": [],
"requested_changes": []
})
.to_string(),
))
.expect("review summary request");
let review_summary_resp = app
.clone()
.oneshot(review_summary_req)
.await
.expect("review summary response");
assert_eq!(review_summary_resp.status(), StatusCode::OK);
let allowed_execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-follow-on-merge/execute-next")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("allowed execute request");
let allowed_execute_resp = app
.clone()
.oneshot(allowed_execute_req)
.await
.expect("allowed execute response");
server.abort();
assert_eq!(allowed_execute_resp.status(), StatusCode::OK);
let allowed_execute_payload: Value = serde_json::from_slice(
&to_bytes(allowed_execute_resp.into_body(), usize::MAX)
.await
.expect("allowed execute body"),
)
.expect("allowed execute json");
assert_eq!(
allowed_execute_payload.get("ok").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
allowed_execute_payload
.get("dispatched")
.and_then(Value::as_bool),
Some(true)
);
}
#[tokio::test]
async fn coder_issue_fix_summary_writes_patch_summary_without_changed_files() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-diagnostic-summary",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "acme/platform"
},
"github_ref": {
"kind": "issue",
"number": 132
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-diagnostic-summary/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"root_cause": "The startup fallback branch was intentionally not patched because the incident was configuration-only.",
"validation_steps": ["cargo test -p tandem-server coder_issue_fix_summary_writes_patch_summary_without_changed_files -- --test-threads=1"],
"validation_results": [{
"kind": "diagnostic",
"status": "passed",
"summary": "Configuration-only recovery path validated without code changes"
}],
"memory_hits_used": ["memory-hit-fix-diagnostic-1"],
"notes": "No-op fix summary for operator follow-up."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_issue_fix_summary")
);
assert_eq!(
summary_payload
.get("validation_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_validation_report")
);
let blackboard = load_context_blackboard(&state, &linked_context_run_id);
let patch_summary_path = blackboard
.artifacts
.iter()
.find(|artifact| artifact.artifact_type == "coder_patch_summary")
.map(|artifact| artifact.path.clone())
.expect("patch summary path");
let patch_summary_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(&patch_summary_path)
.await
.expect("read patch summary artifact"),
)
.expect("parse patch summary artifact");
assert_eq!(
patch_summary_payload
.get("root_cause")
.and_then(Value::as_str),
Some(
"The startup fallback branch was intentionally not patched because the incident was configuration-only."
)
);
assert_eq!(
patch_summary_payload
.get("changed_files")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(0)
);
assert_eq!(
patch_summary_payload
.get("validation_results")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(1)
);
assert_eq!(
patch_summary_payload
.get("worker_run_reference")
.and_then(Value::as_str),
patch_summary_payload
.get("worker_session_context_run_id")
.and_then(Value::as_str)
.or_else(|| {
patch_summary_payload
.get("worker_session_id")
.and_then(Value::as_str)
})
);
assert_eq!(
patch_summary_payload
.get("validation_run_reference")
.and_then(Value::as_str),
patch_summary_payload
.get("validation_session_context_run_id")
.and_then(Value::as_str)
.or_else(|| {
patch_summary_payload
.get("validation_session_id")
.and_then(Value::as_str)
})
);
}
#[tokio::test]
async fn coder_issue_triage_prefers_failure_patterns_in_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-triage-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 65
}
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let triage_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-triage-a/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Crash loop traces point at startup recovery.",
"confidence": "medium"
})
.to_string(),
))
.expect("triage summary request");
let triage_summary_resp = app
.clone()
.oneshot(triage_summary_req)
.await
.expect("triage summary response");
assert_eq!(triage_summary_resp.status(), StatusCode::OK);
let failure_pattern_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-triage-a/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "failure_pattern",
"task_id": "attempt_reproduction",
"summary": "Crash loop consistently starts in startup recovery.",
"payload": {
"workflow_mode": "issue_triage",
"summary": "Crash loop consistently starts in startup recovery.",
"fingerprint": "triage-startup-recovery-loop",
"canonical_markers": ["startup recovery", "crash loop"]
}
})
.to_string(),
))
.expect("failure pattern request");
let failure_pattern_resp = app
.clone()
.oneshot(failure_pattern_req)
.await
.expect("failure pattern response");
assert_eq!(failure_pattern_resp.status(), StatusCode::OK);
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-triage-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 65
}
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-triage-b/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("failure_pattern")
);
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("triage_memory")
&& (row.get("source_coder_run_id").and_then(Value::as_str)
== Some("coder-issue-triage-a")
|| row.get("run_id").and_then(Value::as_str) == Some("coder-issue-triage-a"))
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_issue_fix_reuses_prior_fix_pattern_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-a",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 79
}
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-issue-fix-a/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add the missing startup fallback guard and cover it with a targeted regression test.",
"root_cause": "Startup recovery skipped the nil-config fallback path.",
"fix_strategy": "add startup fallback guard",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression is now covered"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-issue-fix-b",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 79
}
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-issue-fix-b/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload.get("query").and_then(Value::as_str),
Some("user123/tandem issue #79")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("fix_pattern")
);
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("validation_memory")
&& (row.get("source_coder_run_id").and_then(Value::as_str)
== Some("coder-issue-fix-a")
|| row.get("run_id").and_then(Value::as_str) == Some("coder-issue-fix-a"))
}))
.unwrap_or(false));
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("fix_pattern")
&& (row.get("source_coder_run_id").and_then(Value::as_str)
== Some("coder-issue-fix-a")
|| row.get("run_id").and_then(Value::as_str) == Some("coder-issue-fix-a"))
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_pr_review_evidence_advances_review_run() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-evidence",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 87
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let evidence_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-evidence/pr-review-evidence")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "Inspection found a risky migration path and missing rollback test.",
"risk_level": "high",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": ["Rollback test missing"],
"requested_changes": ["Add rollback coverage"],
"regression_signals": [{
"kind": "historical_failure_pattern",
"summary": "Migrations without rollback have failed before"
}],
"memory_hits_used": ["memory-hit-pr-evidence-1"],
"notes": "Evidence recorded before final verdict summary."
})
.to_string(),
))
.expect("evidence request");
let evidence_resp = app
.clone()
.oneshot(evidence_req)
.await
.expect("evidence response");
assert_eq!(evidence_resp.status(), StatusCode::OK);
let evidence_payload: Value = serde_json::from_slice(
&to_bytes(evidence_resp.into_body(), usize::MAX)
.await
.expect("evidence body"),
)
.expect("evidence json");
assert_eq!(
evidence_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_review_evidence")
);
assert_eq!(
evidence_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
assert_eq!(
evidence_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("artifact_write")
);
assert!(evidence_payload
.get("worker_run_reference")
.is_some_and(Value::is_null));
assert!(evidence_payload
.get("worker_session_context_run_id")
.is_some_and(Value::is_null));
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Running);
for workflow_node_id in [
"inspect_pull_request",
"retrieve_memory",
"review_pull_request",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("write_review_artifact"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Runnable)
);
}
#[tokio::test]
async fn coder_pr_review_execute_next_drives_task_runtime_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-execute-next",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 200
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
for expected in [
"inspect_pull_request",
"review_pull_request",
"write_review_artifact",
] {
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-execute-next/execute-next")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test"
})
.to_string(),
))
.expect("execute request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute body"),
)
.expect("execute json");
assert_eq!(
execute_payload
.get("task")
.and_then(|row| row.get("workflow_node_id"))
.and_then(Value::as_str),
Some(expected)
);
if expected != "inspect_pull_request" {
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_run_reference"))
.and_then(Value::as_str),
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session_context_run_id"))
.and_then(Value::as_str)
.or_else(|| {
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session_id"))
.and_then(Value::as_str)
})
);
}
}
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
let blackboard = load_context_blackboard(&state, &linked_context_run_id);
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_pr_review_worker_session"));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_review_evidence"));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_pr_review_summary"));
for workflow_node_id in [
"inspect_pull_request",
"retrieve_memory",
"review_pull_request",
"write_review_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
}
#[tokio::test]
async fn coder_pr_review_summary_create_writes_artifact_and_outcome() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-summary",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 89,
"url": "https://github.com/user123/tandem/pull/89"
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-summary/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "The PR introduces a migration risk and is missing rollback coverage.",
"risk_level": "high",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": ["Missing rollback test"],
"requested_changes": ["Add rollback coverage for the migration path"],
"regression_signals": [{
"kind": "historical_failure_pattern",
"summary": "Similar rollout failed without rollback coverage"
}],
"validation_steps": ["cargo test -p tandem-server coder_pr_review_summary_create_writes_artifact_and_outcome -- --test-threads=1"],
"validation_results": [{
"kind": "targeted_review_validation",
"status": "passed",
"summary": "Targeted review validation passed"
}],
"memory_hits_used": ["memory-hit-1"],
"notes": "Review memory suggests prior migration regressions."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_pr_review_summary")
);
assert_eq!(
summary_payload
.get("review_evidence_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_review_evidence")
);
assert_eq!(
summary_payload
.get("validation_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_validation_report")
);
assert!(summary_payload
.get("worker_run_reference")
.is_some_and(Value::is_null));
assert!(summary_payload
.get("review_evidence_artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.is_some_and(|path| path.ends_with(
"/context_runs/ctx-coder-pr-review-summary/artifacts/pr_review.evidence.json"
)));
assert!(summary_payload
.get("validation_artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.is_some_and(|path| path.ends_with(
"/context_runs/ctx-coder-pr-review-summary/artifacts/pr_review.validation.json"
)));
let summary_artifact_id = summary_payload
.get("artifact")
.and_then(|row| row.get("id"))
.and_then(Value::as_str)
.expect("summary artifact id")
.to_string();
let review_evidence_artifact_id = summary_payload
.get("review_evidence_artifact")
.and_then(|row| row.get("id"))
.and_then(Value::as_str)
.expect("review evidence artifact id")
.to_string();
let validation_artifact_id = summary_payload
.get("validation_artifact")
.and_then(|row| row.get("id"))
.and_then(Value::as_str)
.expect("validation artifact id")
.to_string();
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("review_memory") })),
Some(true)
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("regression_signal")
})),
Some(true)
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("run_outcome") })),
Some(true)
);
let artifacts_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-summary/artifacts")
.body(Body::empty())
.expect("artifacts request");
let artifacts_resp = app
.clone()
.oneshot(artifacts_req)
.await
.expect("artifacts response");
assert_eq!(artifacts_resp.status(), StatusCode::OK);
let artifacts_payload: Value = serde_json::from_slice(
&to_bytes(artifacts_resp.into_body(), usize::MAX)
.await
.expect("artifacts body"),
)
.expect("artifacts json");
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("id").and_then(Value::as_str) == Some(summary_artifact_id.as_str())
&& row.get("artifact_type").and_then(Value::as_str)
== Some("coder_pr_review_summary")
}))
.unwrap_or(false));
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("id").and_then(Value::as_str) == Some(review_evidence_artifact_id.as_str())
&& row.get("artifact_type").and_then(Value::as_str) == Some("coder_review_evidence")
}))
.unwrap_or(false));
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("id").and_then(Value::as_str) == Some(validation_artifact_id.as_str())
&& row.get("artifact_type").and_then(Value::as_str)
== Some("coder_validation_report")
}))
.unwrap_or(false));
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-summary/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
assert!(candidates_payload
.get("candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("review_memory")
&& row
.get("payload")
.and_then(|payload| payload.get("review_evidence_artifact_path"))
.and_then(Value::as_str)
.is_some_and(|path| path.ends_with("/artifacts/pr_review.evidence.json"))
}))
.unwrap_or(false));
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.run_type, "coder_pr_review");
assert_eq!(run.status, ContextRunStatus::Completed);
let workflow_nodes = run
.tasks
.iter()
.filter_map(|task| task.workflow_node_id.clone())
.collect::<Vec<_>>();
for workflow_node_id in [
"inspect_pull_request",
"retrieve_memory",
"review_pull_request",
"write_review_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done; saw workflow nodes: {workflow_nodes:?}"
);
}
}
#[tokio::test]
async fn coder_pr_review_reuses_prior_review_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_baseline_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-baseline",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 90
}
})
.to_string(),
))
.expect("baseline create request");
let create_baseline_resp = app
.clone()
.oneshot(create_baseline_req)
.await
.expect("baseline create response");
assert_eq!(create_baseline_resp.status(), StatusCode::OK);
let baseline_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-baseline/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "comment",
"summary": "Initial review requested one more pass before merge."
})
.to_string(),
))
.expect("baseline summary request");
let baseline_summary_resp = app
.clone()
.oneshot(baseline_summary_req)
.await
.expect("baseline summary response");
assert_eq!(baseline_summary_resp.status(), StatusCode::OK);
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-a",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 90
}
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-a/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "Previous review flagged missing rollback coverage.",
"risk_level": "high",
"requested_changes": ["Add rollback coverage"],
"regression_signals": [{
"kind": "historical_failure_pattern",
"summary": "Rollback-free migrations regressed previously"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-b",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 90
}
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let _create_second_payload: Value = serde_json::from_slice(
&to_bytes(create_second_resp.into_body(), usize::MAX)
.await
.expect("second create body"),
)
.expect("second create json");
let get_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-b")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_payload: Value = serde_json::from_slice(
&to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body"),
)
.expect("get json");
assert!(get_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
}))
.unwrap_or(false));
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-b/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload.get("query").and_then(Value::as_str),
Some("user123/tandem pull request #90 review regressions blockers requested changes")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("review_memory")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("source_coder_run_id"))
.and_then(Value::as_str)
.or_else(|| {
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("run_id"))
.and_then(Value::as_str)
}),
Some("coder-pr-review-a")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("same_ref"))
.and_then(Value::as_bool),
Some(true)
);
assert!(get_payload
.get("memory_hits")
.and_then(|row| row.get("hits"))
.and_then(Value::as_array)
.map(|rows| !rows.is_empty())
.unwrap_or(false));
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("regression_signal")
&& (row.get("source_coder_run_id").and_then(Value::as_str)
== Some("coder-pr-review-a")
|| row.get("run_id").and_then(Value::as_str) == Some("coder-pr-review-a"))
}))
.unwrap_or(false));
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("review_memory")
&& (row.get("source_coder_run_id").and_then(Value::as_str)
== Some("coder-pr-review-a")
|| row.get("run_id").and_then(Value::as_str) == Some("coder-pr-review-a"))
}))
.unwrap_or(false));
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("source_coder_run_id").and_then(Value::as_str) == Some("coder-pr-review-a")
|| row.get("run_id").and_then(Value::as_str) == Some("coder-pr-review-a")
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_merge_recommendation_run_create_gets_seeded_tasks() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-recommendation-1",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 91
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let get_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-recommendation-1")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_payload: Value = serde_json::from_slice(
&to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body"),
)
.expect("get json");
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_merge_recommendation")
);
assert_eq!(
get_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
let tasks = get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.cloned()
.expect("tasks");
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("retrieve_memory"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("done")
);
assert_eq!(
tasks
.iter()
.find(|row| row.get("workflow_node_id").and_then(Value::as_str)
== Some("inspect_pull_request"))
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("runnable")
);
assert!(get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("assess_merge_readiness")
}))
.unwrap_or(false));
assert!(get_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
}))
.unwrap_or(false));
assert_eq!(
get_payload
.get("memory_hits")
.and_then(|row| row.get("query"))
.and_then(Value::as_str),
Some(
"user123/tandem pull request #91 merge recommendation regressions blockers required checks approvals"
)
);
}
#[tokio::test]
async fn coder_merge_readiness_report_advances_merge_run() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-readiness",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 93
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let readiness_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-readiness/merge-readiness-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "The PR is close, but CODEOWNERS approval is still required.",
"risk_level": "medium",
"blockers": ["Required CODEOWNERS approval missing"],
"required_checks": ["ci / test", "ci / lint"],
"required_approvals": ["codeowners"],
"memory_hits_used": ["memory-hit-merge-readiness-1"],
"notes": "Readiness captured before final merge summary."
})
.to_string(),
))
.expect("readiness request");
let readiness_resp = app
.clone()
.oneshot(readiness_req)
.await
.expect("readiness response");
assert_eq!(readiness_resp.status(), StatusCode::OK);
let readiness_payload: Value = serde_json::from_slice(
&to_bytes(readiness_resp.into_body(), usize::MAX)
.await
.expect("readiness body"),
)
.expect("readiness json");
assert_eq!(
readiness_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_merge_readiness_report")
);
assert_eq!(
readiness_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
assert!(readiness_payload
.get("worker_run_reference")
.is_some_and(Value::is_null));
assert!(readiness_payload
.get("worker_session_context_run_id")
.is_some_and(Value::is_null));
assert_eq!(
readiness_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("artifact_write")
);
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Running);
for workflow_node_id in [
"inspect_pull_request",
"retrieve_memory",
"assess_merge_readiness",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("write_merge_artifact"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Runnable)
);
}
#[tokio::test]
async fn coder_merge_recommendation_execute_next_drives_task_runtime_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-execute-next",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 201
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
for expected in [
"inspect_pull_request",
"assess_merge_readiness",
"write_merge_artifact",
] {
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-execute-next/execute-next")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test"
})
.to_string(),
))
.expect("execute request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute body"),
)
.expect("execute json");
assert_eq!(
execute_payload
.get("task")
.and_then(|row| row.get("workflow_node_id"))
.and_then(Value::as_str),
Some(expected)
);
if expected != "inspect_repo" {
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_run_reference"))
.and_then(Value::as_str),
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session_context_run_id"))
.and_then(Value::as_str)
.or_else(|| {
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session_id"))
.and_then(Value::as_str)
})
);
}
}
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
let blackboard = load_context_blackboard(&state, &linked_context_run_id);
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_merge_recommendation_worker_session"));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_merge_readiness_report"));
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_merge_recommendation_summary"));
for workflow_node_id in [
"inspect_pull_request",
"retrieve_memory",
"assess_merge_readiness",
"write_merge_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
}
#[tokio::test]
async fn coder_merge_recommendation_summary_create_writes_artifact() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-recommendation-summary",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 92
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-recommendation-summary/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "Checks are mostly green but one required approval is still missing.",
"risk_level": "medium",
"blockers": ["Required reviewer approval missing"],
"required_checks": ["ci / test", "ci / lint"],
"required_approvals": ["codeowners"],
"validation_steps": ["gh pr checks 92"],
"validation_results": [{
"kind": "merge_gate_validation",
"status": "pending",
"summary": "Required approval still pending"
}],
"memory_hits_used": ["memory-hit-merge-1"],
"notes": "Wait for CODEOWNERS approval before merge."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_merge_recommendation_summary")
);
assert_eq!(
summary_payload
.get("readiness_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_merge_readiness_report")
);
assert_eq!(
summary_payload
.get("validation_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_validation_report")
);
assert_eq!(
summary_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
assert!(summary_payload
.get("worker_run_reference")
.is_some_and(Value::is_null));
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("merge_recommendation_memory")
})),
Some(true)
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("run_outcome") })),
Some(true)
);
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
for workflow_node_id in [
"inspect_pull_request",
"retrieve_memory",
"assess_merge_readiness",
"write_merge_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
let readiness_artifact_id = summary_payload
.get("readiness_artifact")
.and_then(|row| row.get("id"))
.and_then(Value::as_str)
.expect("readiness artifact id")
.to_string();
let validation_artifact_id = summary_payload
.get("validation_artifact")
.and_then(|row| row.get("id"))
.and_then(Value::as_str)
.expect("validation artifact id")
.to_string();
let artifacts_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-recommendation-summary/artifacts")
.body(Body::empty())
.expect("artifacts request");
let artifacts_resp = app
.clone()
.oneshot(artifacts_req)
.await
.expect("artifacts response");
assert_eq!(artifacts_resp.status(), StatusCode::OK);
let artifacts_payload: Value = serde_json::from_slice(
&to_bytes(artifacts_resp.into_body(), usize::MAX)
.await
.expect("artifacts body"),
)
.expect("artifacts json");
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str)
== Some("coder_merge_recommendation_summary")
}))
.unwrap_or(false));
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("id").and_then(Value::as_str) == Some(readiness_artifact_id.as_str())
&& row.get("artifact_type").and_then(Value::as_str)
== Some("coder_merge_readiness_report")
}))
.unwrap_or(false));
assert!(artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("id").and_then(Value::as_str) == Some(validation_artifact_id.as_str())
&& row.get("artifact_type").and_then(Value::as_str)
== Some("coder_validation_report")
}))
.unwrap_or(false));
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-recommendation-summary/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
assert!(candidates_payload
.get("candidates")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("merge_recommendation_memory")
&& row
.get("payload")
.and_then(|payload| payload.get("readiness_artifact_path"))
.and_then(Value::as_str)
.is_some_and(|path| {
path.ends_with("/artifacts/merge_recommendation.readiness.json")
})
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_merge_recommendation_summary_ready_to_merge_awaits_approval() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let mut rx = state.event_bus.subscribe();
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-ready-for-approval",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 193
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-ready-for-approval/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "All required checks and approvals are satisfied.",
"risk_level": "low",
"blockers": [],
"required_checks": [],
"required_approvals": [],
"memory_hits_used": ["memory-hit-merge-ready-1"],
"notes": "Ready for operator approval."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("approval_required")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
summary_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("awaiting_approval")
);
assert_eq!(
summary_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("approval")
);
let approval_event = next_event_of_type(&mut rx, "coder.approval.required").await;
assert_eq!(
approval_event
.properties
.get("event_type")
.and_then(Value::as_str),
Some("merge_recommendation_ready")
);
assert_eq!(
approval_event
.properties
.get("recommendation")
.and_then(Value::as_str),
Some("merge")
);
assert_eq!(
approval_event
.properties
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approval_event
.properties
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("requires_merge_execution_request")
);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-ready-for-approval/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved the merge recommendation."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let approve_payload: Value = serde_json::from_slice(
&to_bytes(approve_resp.into_body(), usize::MAX)
.await
.expect("approve body"),
)
.expect("approve json");
assert_eq!(
approve_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
assert_eq!(
approve_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("completed")
);
assert_eq!(
approve_payload
.get("event")
.and_then(|row| row.get("type"))
.and_then(Value::as_str),
Some("merge_recommendation_approved")
);
assert_eq!(
approve_payload
.get("merge_execution_request")
.and_then(|row| row.get("recommendation"))
.and_then(Value::as_str),
Some("merge")
);
assert!(approve_payload
.get("worker_run_reference")
.is_some_and(Value::is_null));
assert!(approve_payload
.get("worker_session_context_run_id")
.is_some_and(Value::is_null));
assert!(approve_payload
.get("validation_run_reference")
.is_some_and(Value::is_null));
assert!(approve_payload
.get("validation_session_context_run_id")
.is_some_and(Value::is_null));
assert_eq!(
approve_payload
.get("merge_execution_artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_merge_execution_request")
);
let merge_execution_artifact_path = approve_payload
.get("merge_execution_artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.expect("merge execution artifact path");
let merge_execution_artifact_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(merge_execution_artifact_path)
.await
.expect("read merge execution artifact"),
)
.expect("parse merge execution artifact");
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("preferred_submit_mode"))
.and_then(Value::as_str),
Some("manual")
);
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("explicit_submit_required"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("auto_execute_after_approval"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("auto_execute_eligible"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("project_auto_merge_policy_disabled")
);
assert_eq!(
merge_execution_artifact_payload
.get("merge_submit_policy_preview")
.and_then(|row| row.get("manual"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("preferred_submit_mode"))
.and_then(Value::as_str),
Some("manual")
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("explicit_submit_required"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_after_approval"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_eligible"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("project_auto_merge_policy_disabled")
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("manual"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto"))
.and_then(|row| row.get("policy"))
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_explicit_auto_merge_submit_opt_in")
);
let merge_event = next_event_of_type(&mut rx, "coder.merge.recommended").await;
assert_eq!(
merge_event
.properties
.get("event_type")
.and_then(Value::as_str),
Some("merge_execution_request_ready")
);
assert_eq!(
merge_event
.properties
.get("recommendation")
.and_then(Value::as_str),
Some("merge")
);
assert_eq!(
merge_event
.properties
.get("merge_submit_policy")
.and_then(|row| row.get("preferred_submit_mode"))
.and_then(Value::as_str),
Some("manual")
);
assert_eq!(
merge_event
.properties
.get("merge_submit_policy")
.and_then(|row| row.get("explicit_submit_required"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
merge_event
.properties
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_eligible"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
merge_event
.properties
.get("merge_submit_policy")
.and_then(|row| row.get("auto"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(true)
);
}
#[tokio::test]
async fn coder_merge_submit_real_submit_writes_merge_artifact() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let mut rx = state.event_bus.subscribe();
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-submit-real",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 314
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-real/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Checks and approvals are complete.",
"risk_level": "low",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-real/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-real/merge-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Execute the approved merge",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("submitted").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
submit_payload
.get("merged_github_ref")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("pull_request")
);
assert_eq!(
submit_payload
.get("merge_result")
.and_then(|row| row.get("merged"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
submit_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_merge_submission")
);
assert_eq!(
submit_payload
.get("external_action")
.and_then(|row| row.get("capability_id"))
.and_then(Value::as_str),
Some("github.merge_pull_request")
);
assert_eq!(
submit_payload
.get("external_action")
.and_then(|row| row.get("source_kind"))
.and_then(Value::as_str),
Some("coder")
);
assert!(submit_payload
.get("worker_run_reference")
.is_some_and(Value::is_null));
assert!(submit_payload
.get("worker_session_context_run_id")
.is_some_and(Value::is_null));
assert!(submit_payload
.get("validation_run_reference")
.is_some_and(Value::is_null));
assert!(submit_payload
.get("validation_session_context_run_id")
.is_some_and(Value::is_null));
let merge_submit_event = next_event_of_type(&mut rx, "coder.merge.submitted").await;
assert_eq!(
merge_submit_event
.properties
.get("merged_github_ref")
.and_then(|row| row.get("number"))
.and_then(Value::as_u64),
Some(314)
);
}
#[tokio::test]
async fn coder_project_policy_get_and_put_controls_auto_merge_flag() {
let state = test_state().await;
let app = app_router(state.clone());
let get_before_req = Request::builder()
.method("GET")
.uri("/coder/projects/proj-engine/policy")
.body(Body::empty())
.expect("get before request");
let get_before_resp = app
.clone()
.oneshot(get_before_req)
.await
.expect("get before response");
assert_eq!(get_before_resp.status(), StatusCode::OK);
let get_before_payload: Value = serde_json::from_slice(
&to_bytes(get_before_resp.into_body(), usize::MAX)
.await
.expect("get before body"),
)
.expect("get before json");
assert_eq!(
get_before_payload
.get("project_policy")
.and_then(|row| row.get("project_id"))
.and_then(Value::as_str),
Some("proj-engine")
);
assert_eq!(
get_before_payload
.get("project_policy")
.and_then(|row| row.get("auto_merge_enabled"))
.and_then(Value::as_bool),
Some(false)
);
let put_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/policy")
.header("content-type", "application/json")
.body(Body::from(
json!({
"auto_merge_enabled": true
})
.to_string(),
))
.expect("put request");
let put_resp = app.clone().oneshot(put_req).await.expect("put response");
assert_eq!(put_resp.status(), StatusCode::OK);
let put_payload: Value = serde_json::from_slice(
&to_bytes(put_resp.into_body(), usize::MAX)
.await
.expect("put body"),
)
.expect("put json");
assert_eq!(put_payload.get("ok").and_then(Value::as_bool), Some(true));
assert_eq!(
put_payload
.get("project_policy")
.and_then(|row| row.get("auto_merge_enabled"))
.and_then(Value::as_bool),
Some(true)
);
let get_after_req = Request::builder()
.method("GET")
.uri("/coder/projects/proj-engine/policy")
.body(Body::empty())
.expect("get after request");
let get_after_resp = app
.clone()
.oneshot(get_after_req)
.await
.expect("get after response");
assert_eq!(get_after_resp.status(), StatusCode::OK);
let get_after_payload: Value = serde_json::from_slice(
&to_bytes(get_after_resp.into_body(), usize::MAX)
.await
.expect("get after body"),
)
.expect("get after json");
assert_eq!(
get_after_payload
.get("project_policy")
.and_then(|row| row.get("auto_merge_enabled"))
.and_then(Value::as_bool),
Some(true)
);
}
#[tokio::test]
async fn coder_project_list_summarizes_known_repo_bindings_and_policy() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let policy_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/policy")
.header("content-type", "application/json")
.body(Body::from(
json!({
"auto_merge_enabled": true
})
.to_string(),
))
.expect("policy request");
let policy_resp = app
.clone()
.oneshot(policy_req)
.await
.expect("policy response");
assert_eq!(policy_resp.status(), StatusCode::OK);
for payload in [
json!({
"coder_run_id": "coder-project-list-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 321
},
"mcp_servers": ["github"]
}),
json!({
"coder_run_id": "coder-project-list-b",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 322
},
"mcp_servers": ["github"]
}),
json!({
"coder_run_id": "coder-project-list-c",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-docs",
"workspace_id": "ws-docs",
"workspace_root": "/tmp/docs-repo",
"repo_slug": "user123/docs"
},
"github_ref": {
"kind": "pull_request",
"number": 12
},
"mcp_servers": ["github"]
}),
] {
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(payload.to_string()))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
}
let list_req = Request::builder()
.method("GET")
.uri("/coder/projects")
.body(Body::empty())
.expect("list request");
let list_resp = app.clone().oneshot(list_req).await.expect("list response");
server.abort();
assert_eq!(list_resp.status(), StatusCode::OK);
let list_payload: Value = serde_json::from_slice(
&to_bytes(list_resp.into_body(), usize::MAX)
.await
.expect("list body"),
)
.expect("list json");
let projects = list_payload
.get("projects")
.and_then(Value::as_array)
.expect("projects array");
assert_eq!(projects.len(), 2);
let engine_project = projects
.iter()
.find(|row| row.get("project_id").and_then(Value::as_str) == Some("proj-engine"))
.expect("engine project");
let docs_project = projects
.iter()
.find(|row| row.get("project_id").and_then(Value::as_str) == Some("proj-docs"))
.expect("docs project");
assert_eq!(
engine_project.get("project_id").and_then(Value::as_str),
Some("proj-engine")
);
assert_eq!(
engine_project.get("run_count").and_then(Value::as_u64),
Some(2)
);
assert_eq!(
engine_project
.get("project_policy")
.and_then(|row| row.get("auto_merge_enabled"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
engine_project
.get("workflow_modes")
.and_then(Value::as_array)
.map(|rows| rows.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
Some(vec!["issue_fix", "issue_triage"])
);
assert_eq!(
docs_project.get("project_id").and_then(Value::as_str),
Some("proj-docs")
);
assert_eq!(
docs_project
.get("project_policy")
.and_then(|row| row.get("auto_merge_enabled"))
.and_then(Value::as_bool),
Some(false)
);
}
#[tokio::test]
async fn coder_project_binding_get_put_and_project_list_prefers_explicit_binding() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let put_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"project_id": "ignored-by-endpoint",
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit"
})
.to_string(),
))
.expect("put request");
let put_resp = app.clone().oneshot(put_req).await.expect("put response");
assert_eq!(put_resp.status(), StatusCode::OK);
let put_payload: Value = serde_json::from_slice(
&to_bytes(put_resp.into_body(), usize::MAX)
.await
.expect("put body"),
)
.expect("put json");
assert_eq!(
put_payload
.get("binding")
.and_then(|row| row.get("repo_binding"))
.and_then(|row| row.get("project_id"))
.and_then(Value::as_str),
Some("proj-engine")
);
let get_req = Request::builder()
.method("GET")
.uri("/coder/projects/proj-engine/bindings")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_payload: Value = serde_json::from_slice(
&to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body"),
)
.expect("get json");
assert_eq!(
get_payload
.get("binding")
.and_then(|row| row.get("repo_binding"))
.and_then(|row| row.get("repo_slug"))
.and_then(Value::as_str),
Some("user123/tandem-explicit")
);
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-project-binding-run",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-derived",
"workspace_root": "/tmp/derived-repo",
"repo_slug": "user123/tandem-derived"
},
"github_ref": {
"kind": "issue",
"number": 325
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let list_req = Request::builder()
.method("GET")
.uri("/coder/projects")
.body(Body::empty())
.expect("list request");
let list_resp = app.clone().oneshot(list_req).await.expect("list response");
server.abort();
assert_eq!(list_resp.status(), StatusCode::OK);
let list_payload: Value = serde_json::from_slice(
&to_bytes(list_resp.into_body(), usize::MAX)
.await
.expect("list body"),
)
.expect("list json");
let engine_project = list_payload
.get("projects")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("project_id").and_then(Value::as_str) == Some("proj-engine"))
})
.expect("engine project");
assert_eq!(
engine_project
.get("repo_binding")
.and_then(|row| row.get("repo_slug"))
.and_then(Value::as_str),
Some("user123/tandem-explicit")
);
assert_eq!(
engine_project
.get("repo_binding")
.and_then(|row| row.get("workspace_root"))
.and_then(Value::as_str),
Some("/tmp/explicit-repo")
);
}
#[tokio::test]
async fn coder_project_binding_put_bootstraps_github_mcp_server_from_auth() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
assert!(state.mcp.remove("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
assert!(
super::super::ensure_remote_mcp_server(
&state,
"github",
&endpoint,
std::collections::HashMap::from([(
"Authorization".to_string(),
"Bearer test-token".to_string(),
)]),
)
.await
);
let app = app_router(state.clone());
let put_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"repo_binding": {
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit"
},
"github_project_binding": {
"owner": "user123",
"project_number": 42
}
})
.to_string(),
))
.expect("put request");
let put_resp = app.clone().oneshot(put_req).await.expect("put response");
server.abort();
assert_eq!(put_resp.status(), StatusCode::OK);
let put_payload: Value = serde_json::from_slice(
&to_bytes(put_resp.into_body(), usize::MAX)
.await
.expect("put body"),
)
.expect("put json");
assert_eq!(
put_payload
.get("binding")
.and_then(|row| row.get("github_project_binding"))
.and_then(|row| row.get("mcp_server"))
.and_then(Value::as_str),
Some("github")
);
}
#[tokio::test]
async fn coder_project_binding_put_discovers_github_project_schema() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let put_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"repo_binding": {
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit"
},
"github_project_binding": {
"owner": "user123",
"project_number": 42,
"mcp_server": "github"
}
})
.to_string(),
))
.expect("put request");
let put_resp = app.clone().oneshot(put_req).await.expect("put response");
server.abort();
assert_eq!(put_resp.status(), StatusCode::OK);
let put_payload: Value = serde_json::from_slice(
&to_bytes(put_resp.into_body(), usize::MAX)
.await
.expect("put body"),
)
.expect("put json");
assert_eq!(
put_payload
.get("binding")
.and_then(|row| row.get("github_project_binding"))
.and_then(|row| row.get("schema_fingerprint"))
.and_then(Value::as_str)
.map(|row| !row.is_empty()),
Some(true)
);
assert_eq!(
put_payload
.get("binding")
.and_then(|row| row.get("github_project_binding"))
.and_then(|row| row.get("status_mapping"))
.and_then(|row| row.get("todo"))
.and_then(|row| row.get("name"))
.and_then(Value::as_str),
Some("TODO")
);
}
#[tokio::test]
async fn coder_project_github_project_inbox_lists_actionable_and_unsupported_items() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let binding_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"repo_binding": {
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit"
},
"github_project_binding": {
"owner": "user123",
"project_number": 42,
"mcp_server": "github"
}
})
.to_string(),
))
.expect("binding request");
let binding_resp = app
.clone()
.oneshot(binding_req)
.await
.expect("binding response");
assert_eq!(binding_resp.status(), StatusCode::OK);
let inbox_req = Request::builder()
.method("GET")
.uri("/coder/projects/proj-engine/github-project/inbox")
.body(Body::empty())
.expect("inbox request");
let inbox_resp = app
.clone()
.oneshot(inbox_req)
.await
.expect("inbox response");
server.abort();
assert_eq!(inbox_resp.status(), StatusCode::OK);
let inbox_payload: Value = serde_json::from_slice(
&to_bytes(inbox_resp.into_body(), usize::MAX)
.await
.expect("inbox body"),
)
.expect("inbox json");
let items = inbox_payload
.get("items")
.and_then(Value::as_array)
.expect("items");
assert_eq!(items.len(), 2);
assert_eq!(
items[0].get("actionable").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
items[1].get("unsupported_reason").and_then(Value::as_str),
Some("unsupported_item_type")
);
}
#[tokio::test]
async fn coder_project_github_project_intake_is_idempotent_for_active_item() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let binding_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"repo_binding": {
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit"
},
"github_project_binding": {
"owner": "user123",
"project_number": 42,
"mcp_server": "github"
}
})
.to_string(),
))
.expect("binding request");
let binding_resp = app
.clone()
.oneshot(binding_req)
.await
.expect("binding response");
assert_eq!(binding_resp.status(), StatusCode::OK);
let first_req = Request::builder()
.method("POST")
.uri("/coder/projects/proj-engine/github-project/intake")
.header("content-type", "application/json")
.body(Body::from(
json!({
"project_item_id": "PVT_item_1",
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("first intake request");
let first_resp = app
.clone()
.oneshot(first_req)
.await
.expect("first intake response");
assert_eq!(first_resp.status(), StatusCode::OK);
let first_payload: Value = serde_json::from_slice(
&to_bytes(first_resp.into_body(), usize::MAX)
.await
.expect("first intake body"),
)
.expect("first intake json");
let first_run_id = first_payload
.get("coder_run")
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str)
.expect("first coder run id")
.to_string();
assert_eq!(
first_payload
.get("coder_run")
.and_then(|row| row.get("github_project_ref"))
.and_then(|row| row.get("project_item_id"))
.and_then(Value::as_str),
Some("PVT_item_1")
);
let second_req = Request::builder()
.method("POST")
.uri("/coder/projects/proj-engine/github-project/intake")
.header("content-type", "application/json")
.body(Body::from(
json!({
"project_item_id": "PVT_item_1",
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("second intake request");
let second_resp = app
.clone()
.oneshot(second_req)
.await
.expect("second intake response");
server.abort();
assert_eq!(second_resp.status(), StatusCode::OK);
let second_payload: Value = serde_json::from_slice(
&to_bytes(second_resp.into_body(), usize::MAX)
.await
.expect("second intake body"),
)
.expect("second intake json");
assert_eq!(
second_payload.get("deduped").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
second_payload
.get("coder_run")
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str),
Some(first_run_id.as_str())
);
}
#[tokio::test]
async fn coder_project_get_returns_policy_binding_and_recent_runs() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let policy_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/policy")
.header("content-type", "application/json")
.body(Body::from(
json!({
"auto_merge_enabled": true
})
.to_string(),
))
.expect("policy request");
let policy_resp = app
.clone()
.oneshot(policy_req)
.await
.expect("policy response");
assert_eq!(policy_resp.status(), StatusCode::OK);
let binding_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"project_id": "ignored-by-endpoint",
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit",
"default_branch": "main"
})
.to_string(),
))
.expect("binding request");
let binding_resp = app
.clone()
.oneshot(binding_req)
.await
.expect("binding response");
assert_eq!(binding_resp.status(), StatusCode::OK);
for (coder_run_id, workflow_mode, number) in [
("coder-project-detail-triage", "issue_triage", 41_u64),
("coder-project-detail-fix", "issue_fix", 42_u64),
] {
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": coder_run_id,
"workflow_mode": workflow_mode,
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-derived",
"workspace_root": "/tmp/derived-repo",
"repo_slug": "user123/tandem-derived"
},
"github_ref": {
"kind": "issue",
"number": number
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
}
let get_req = Request::builder()
.method("GET")
.uri("/coder/projects/proj-engine")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
server.abort();
assert_eq!(get_resp.status(), StatusCode::OK);
let get_payload: Value = serde_json::from_slice(
&to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body"),
)
.expect("get json");
assert_eq!(
get_payload
.get("project")
.and_then(|row| row.get("repo_binding"))
.and_then(|row| row.get("repo_slug"))
.and_then(Value::as_str),
Some("user123/tandem-explicit")
);
assert_eq!(
get_payload
.get("binding")
.and_then(|row| row.get("repo_binding"))
.and_then(|row| row.get("workspace_root"))
.and_then(Value::as_str),
Some("/tmp/explicit-repo")
);
assert_eq!(
get_payload
.get("project_policy")
.and_then(|row| row.get("auto_merge_enabled"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
get_payload
.get("project")
.and_then(|row| row.get("run_count"))
.and_then(Value::as_u64),
Some(2)
);
assert_eq!(
get_payload
.get("project")
.and_then(|row| row.get("workflow_modes"))
.cloned(),
Some(json!(["issue_fix", "issue_triage"]))
);
let recent_runs = get_payload
.get("recent_runs")
.and_then(Value::as_array)
.expect("recent runs");
assert_eq!(recent_runs.len(), 2);
assert_eq!(
recent_runs
.first()
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_fix")
);
assert!(
recent_runs
.first()
.and_then(|row| row.get("execution_policy"))
.map(Value::is_object)
.unwrap_or(false),
"expected execution policy on recent run"
);
}
#[tokio::test]
async fn coder_project_run_create_uses_saved_binding_and_requires_it() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let missing_binding_req = Request::builder()
.method("POST")
.uri("/coder/projects/proj-missing/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "issue_triage",
"github_ref": {
"kind": "issue",
"number": 7
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("missing binding request");
let missing_binding_resp = app
.clone()
.oneshot(missing_binding_req)
.await
.expect("missing binding response");
assert_eq!(missing_binding_resp.status(), StatusCode::CONFLICT);
let missing_binding_payload: Value = serde_json::from_slice(
&to_bytes(missing_binding_resp.into_body(), usize::MAX)
.await
.expect("missing binding body"),
)
.expect("missing binding json");
assert_eq!(
missing_binding_payload.get("code").and_then(Value::as_str),
Some("CODER_PROJECT_BINDING_REQUIRED")
);
let binding_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/bindings")
.header("content-type", "application/json")
.body(Body::from(
json!({
"project_id": "ignored-by-endpoint",
"workspace_id": "ws-explicit",
"workspace_root": "/tmp/explicit-repo",
"repo_slug": "user123/tandem-explicit",
"default_branch": "main"
})
.to_string(),
))
.expect("binding request");
let binding_resp = app
.clone()
.oneshot(binding_req)
.await
.expect("binding response");
assert_eq!(binding_resp.status(), StatusCode::OK);
let create_req = Request::builder()
.method("POST")
.uri("/coder/projects/proj-engine/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-project-scoped-run",
"workflow_mode": "issue_triage",
"github_ref": {
"kind": "issue",
"number": 91
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
server.abort();
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
assert_eq!(
create_payload
.get("coder_run")
.and_then(|row| row.get("repo_binding"))
.and_then(|row| row.get("repo_slug"))
.and_then(Value::as_str),
Some("user123/tandem-explicit")
);
assert_eq!(
create_payload
.get("coder_run")
.and_then(|row| row.get("repo_binding"))
.and_then(|row| row.get("workspace_root"))
.and_then(Value::as_str),
Some("/tmp/explicit-repo")
);
}
#[tokio::test]
async fn coder_project_run_list_filters_to_project_and_sorts_newest_first() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
for (coder_run_id, project_id, workflow_mode, number) in [
(
"coder-project-runs-triage",
"proj-engine",
"issue_triage",
51_u64,
),
("coder-project-runs-fix", "proj-engine", "issue_fix", 52_u64),
(
"coder-project-runs-review",
"proj-other",
"pr_review",
53_u64,
),
] {
let kind = if workflow_mode == "pr_review" {
"pull_request"
} else {
"issue"
};
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": coder_run_id,
"workflow_mode": workflow_mode,
"repo_binding": {
"project_id": project_id,
"workspace_id": format!("ws-{project_id}"),
"workspace_root": format!("/tmp/{project_id}"),
"repo_slug": format!("user123/{project_id}")
},
"github_ref": {
"kind": kind,
"number": number
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
}
let list_req = Request::builder()
.method("GET")
.uri("/coder/projects/proj-engine/runs?limit=10")
.body(Body::empty())
.expect("list request");
let list_resp = app.clone().oneshot(list_req).await.expect("list response");
server.abort();
assert_eq!(list_resp.status(), StatusCode::OK);
let list_payload: Value = serde_json::from_slice(
&to_bytes(list_resp.into_body(), usize::MAX)
.await
.expect("list body"),
)
.expect("list json");
assert_eq!(
list_payload.get("project_id").and_then(Value::as_str),
Some("proj-engine")
);
let runs = list_payload
.get("runs")
.and_then(Value::as_array)
.expect("runs");
assert_eq!(runs.len(), 2);
assert_eq!(
runs.first()
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_fix")
);
assert_eq!(
runs.get(1)
.and_then(|row| row.get("coder_run"))
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_triage")
);
assert!(
runs.iter().all(|row| {
row.get("coder_run")
.and_then(|coder_run| coder_run.get("repo_binding"))
.and_then(|binding| binding.get("project_id"))
.and_then(Value::as_str)
== Some("proj-engine")
}),
"expected only proj-engine runs"
);
}
#[tokio::test]
async fn coder_status_summarizes_active_and_approval_runs() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
for payload in [
json!({
"coder_run_id": "coder-status-issue-fix",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 323
},
"mcp_servers": ["github"]
}),
json!({
"coder_run_id": "coder-status-merge",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 324
},
"mcp_servers": ["github"]
}),
] {
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(payload.to_string()))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
}
let merge_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-status-merge/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Everything looks ready from the merge side.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("merge summary request");
let merge_summary_resp = app
.clone()
.oneshot(merge_summary_req)
.await
.expect("merge summary response");
assert_eq!(merge_summary_resp.status(), StatusCode::OK);
let status_req = Request::builder()
.method("GET")
.uri("/coder/status")
.body(Body::empty())
.expect("status request");
let status_resp = app
.clone()
.oneshot(status_req)
.await
.expect("status response");
server.abort();
assert_eq!(status_resp.status(), StatusCode::OK);
let status_payload: Value = serde_json::from_slice(
&to_bytes(status_resp.into_body(), usize::MAX)
.await
.expect("status body"),
)
.expect("status json");
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("total_runs"))
.and_then(Value::as_u64),
Some(2)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("active_runs"))
.and_then(Value::as_u64),
Some(2)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("awaiting_approval_runs"))
.and_then(Value::as_u64),
Some(1)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("project_count"))
.and_then(Value::as_u64),
Some(1)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("workflow_counts"))
.and_then(|row| row.get("issue_fix"))
.and_then(Value::as_u64),
Some(1)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("workflow_counts"))
.and_then(|row| row.get("merge_recommendation"))
.and_then(Value::as_u64),
Some(1)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("run_status_counts"))
.and_then(|row| row.get("running"))
.and_then(Value::as_u64),
Some(1)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("run_status_counts"))
.and_then(|row| row.get("awaiting_approval"))
.and_then(Value::as_u64),
Some(1)
);
assert_eq!(
status_payload
.get("status")
.and_then(|row| row.get("latest_run"))
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str),
Some("coder-status-merge")
);
}
#[tokio::test]
async fn coder_merge_submit_blocks_when_execution_request_is_not_merge_ready() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-submit-blocked",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 315
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-blocked/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "This looked merge-ready before downstream policy re-check.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-blocked/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let approve_payload: Value = serde_json::from_slice(
&to_bytes(approve_resp.into_body(), usize::MAX)
.await
.expect("approve body"),
)
.expect("approve json");
let merge_execution_artifact_path = approve_payload
.get("merge_execution_artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.expect("merge execution artifact path")
.to_string();
tokio::fs::write(
&merge_execution_artifact_path,
serde_json::to_string_pretty(&json!({
"coder_run_id": "coder-merge-submit-blocked",
"linked_context_run_id": "ctx-coder-merge-submit-blocked",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 315
},
"recommendation": "hold",
"blockers": ["Manual verification pending"],
"required_checks": ["ci / test"],
"required_approvals": ["codeowners"]
}))
.expect("merge execution artifact json"),
)
.await
.expect("overwrite merge execution artifact");
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-blocked/merge-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Try to merge anyway",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("ok").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload.get("code").and_then(Value::as_str),
Some("CODER_MERGE_SUBMIT_POLICY_BLOCKED")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("merge_execution_request_not_merge_ready")
);
}
#[tokio::test]
async fn coder_merge_submit_blocks_auto_mode_without_opt_in() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-submit-auto-blocked",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 316
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-auto-blocked/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Checks and approvals are complete.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-auto-blocked/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-auto-blocked/merge-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Try to auto-execute the merge",
"submit_mode": "auto",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("ok").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload.get("code").and_then(Value::as_str),
Some("CODER_MERGE_SUBMIT_POLICY_BLOCKED")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_explicit_auto_merge_submit_opt_in")
);
}
#[tokio::test]
async fn coder_merge_submit_blocks_auto_mode_for_manual_follow_on() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-submit-manual-follow-on-parent",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 319
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-parent/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add startup fallback for nil config handling.",
"root_cause": "Missing fallback in recovery path.",
"fix_strategy": "Restore fallback and add regression coverage.",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup fallback regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-parent/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_pr_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-parent/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Open the draft PR",
"dry_run": false,
"mcp_server": "github",
"allow_auto_merge_recommendation": true
})
.to_string(),
))
.expect("submit pr request");
let submit_pr_resp = app
.clone()
.oneshot(submit_pr_req)
.await
.expect("submit pr response");
assert_eq!(submit_pr_resp.status(), StatusCode::OK);
let merge_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "merge_recommendation",
"coder_run_id": "coder-merge-submit-manual-follow-on-merge"
})
.to_string(),
))
.expect("merge follow-on request");
let merge_follow_on_resp = app
.clone()
.oneshot(merge_follow_on_req)
.await
.expect("merge follow-on response");
assert_eq!(merge_follow_on_resp.status(), StatusCode::OK);
let merge_follow_on_payload: Value = serde_json::from_slice(
&to_bytes(merge_follow_on_resp.into_body(), usize::MAX)
.await
.expect("merge follow-on body"),
)
.expect("merge follow-on json");
assert_eq!(
merge_follow_on_payload
.get("coder_run")
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("spawn_mode"))
.and_then(Value::as_str),
Some("manual")
);
assert_eq!(
merge_follow_on_payload
.get("coder_run")
.and_then(|row| row.get("origin_policy"))
.and_then(|row| row.get("merge_auto_spawn_opted_in"))
.and_then(Value::as_bool),
Some(true)
);
let merge_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-merge/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Everything looks ready from the merge side.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("merge summary request");
let merge_summary_resp = app
.clone()
.oneshot(merge_summary_req)
.await
.expect("merge summary response");
assert_eq!(merge_summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-merge/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let approve_payload: Value = serde_json::from_slice(
&to_bytes(approve_resp.into_body(), usize::MAX)
.await
.expect("approve body"),
)
.expect("approve json");
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("preferred_submit_mode"))
.and_then(Value::as_str),
Some("manual")
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("explicit_submit_required"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_eligible"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("requires_approved_pr_review_follow_on")
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto"))
.and_then(|row| row.get("policy"))
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_auto_spawned_merge_follow_on")
);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-manual-follow-on-merge/merge-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Try to auto-execute the manual follow-on merge",
"submit_mode": "auto",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("ok").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload.get("code").and_then(Value::as_str),
Some("CODER_MERGE_SUBMIT_POLICY_BLOCKED")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_auto_spawned_merge_follow_on")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("spawn_mode"))
.and_then(Value::as_str),
Some("manual")
);
}
#[tokio::test]
async fn coder_merge_policy_reports_auto_execute_eligibility_when_project_enabled() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let policy_req = Request::builder()
.method("PUT")
.uri("/coder/projects/proj-engine/policy")
.header("content-type", "application/json")
.body(Body::from(
json!({
"auto_merge_enabled": true
})
.to_string(),
))
.expect("policy request");
let policy_resp = app
.clone()
.oneshot(policy_req)
.await
.expect("policy response");
assert_eq!(policy_resp.status(), StatusCode::OK);
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-auto-eligible-parent",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 320
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-auto-eligible-parent/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add missing fallback to startup recovery.",
"root_cause": "Recovery skipped the nil-config guard.",
"fix_strategy": "restore startup fallback and add a targeted regression",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-auto-eligible-parent/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_pr_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-auto-eligible-parent/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Open the draft PR and queue review plus merge follow-ons",
"dry_run": false,
"mcp_server": "github",
"allow_auto_merge_recommendation": true,
"spawn_follow_on_runs": ["merge_recommendation"]
})
.to_string(),
))
.expect("submit pr request");
let submit_pr_resp = app
.clone()
.oneshot(submit_pr_req)
.await
.expect("submit pr response");
assert_eq!(submit_pr_resp.status(), StatusCode::OK);
let submit_pr_payload: Value = serde_json::from_slice(
&to_bytes(submit_pr_resp.into_body(), usize::MAX)
.await
.expect("submit pr body"),
)
.expect("submit pr json");
let spawned_runs = submit_pr_payload
.get("spawned_follow_on_runs")
.and_then(Value::as_array)
.expect("spawned follow-on runs");
assert_eq!(spawned_runs.len(), 2);
let review_run_id = spawned_runs[0]
.get("coder_run")
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str)
.expect("review run id");
let merge_run_id = spawned_runs[1]
.get("coder_run")
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str)
.expect("merge run id");
let review_summary_req = Request::builder()
.method("POST")
.uri(&format!("/coder/runs/{review_run_id}/pr-review-summary"))
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "approve",
"summary": "Looks good to merge.",
"risk_level": "low",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": [],
"requested_changes": [],
"regression_signals": []
})
.to_string(),
))
.expect("review summary request");
let review_summary_resp = app
.clone()
.oneshot(review_summary_req)
.await
.expect("review summary response");
assert_eq!(review_summary_resp.status(), StatusCode::OK);
let merge_summary_req = Request::builder()
.method("POST")
.uri(&format!(
"/coder/runs/{merge_run_id}/merge-recommendation-summary"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Everything looks ready from the merge side.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("merge summary request");
let merge_summary_resp = app
.clone()
.oneshot(merge_summary_req)
.await
.expect("merge summary response");
assert_eq!(merge_summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri(&format!("/coder/runs/{merge_run_id}/approve"))
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
server.abort();
assert_eq!(approve_resp.status(), StatusCode::OK);
let approve_payload: Value = serde_json::from_slice(
&to_bytes(approve_resp.into_body(), usize::MAX)
.await
.expect("approve body"),
)
.expect("approve json");
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("preferred_submit_mode"))
.and_then(Value::as_str),
Some("auto")
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_policy_enabled"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_eligible"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_after_approval"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto_execute_block_reason"))
.and_then(Value::as_str),
Some("explicit_submit_required_policy")
);
assert_eq!(
approve_payload
.get("merge_submit_policy")
.and_then(|row| row.get("auto"))
.and_then(|row| row.get("blocked"))
.and_then(Value::as_bool),
Some(false)
);
}
#[tokio::test]
async fn coder_merge_submit_blocks_without_approved_sibling_pr_review() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-submit-review-block-parent",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 317
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-parent/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add startup fallback for nil config handling.",
"root_cause": "Missing fallback in recovery path.",
"fix_strategy": "Restore fallback and add regression coverage.",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup fallback regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-parent/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_pr_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-parent/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Open the draft PR",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit pr request");
let submit_pr_resp = app
.clone()
.oneshot(submit_pr_req)
.await
.expect("submit pr response");
assert_eq!(submit_pr_resp.status(), StatusCode::OK);
let review_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "pr_review",
"coder_run_id": "coder-merge-submit-review-block-review"
})
.to_string(),
))
.expect("review follow-on request");
let review_follow_on_resp = app
.clone()
.oneshot(review_follow_on_req)
.await
.expect("review follow-on response");
assert_eq!(review_follow_on_resp.status(), StatusCode::OK);
let review_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-review/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "Rollback coverage is still missing.",
"risk_level": "medium",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": [],
"requested_changes": ["Add rollback coverage"],
"regression_signals": []
})
.to_string(),
))
.expect("review summary request");
let review_summary_resp = app
.clone()
.oneshot(review_summary_req)
.await
.expect("review summary response");
assert_eq!(review_summary_resp.status(), StatusCode::OK);
let merge_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "merge_recommendation",
"coder_run_id": "coder-merge-submit-review-block-merge"
})
.to_string(),
))
.expect("merge follow-on request");
let merge_follow_on_resp = app
.clone()
.oneshot(merge_follow_on_req)
.await
.expect("merge follow-on response");
assert_eq!(merge_follow_on_resp.status(), StatusCode::OK);
let merge_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-merge/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Everything looks ready from the merge side.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("merge summary request");
let merge_summary_resp = app
.clone()
.oneshot(merge_summary_req)
.await
.expect("merge summary response");
assert_eq!(merge_summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-merge/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-review-block-merge/merge-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Try to merge despite review objections",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("ok").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload.get("code").and_then(Value::as_str),
Some("CODER_MERGE_SUBMIT_POLICY_BLOCKED")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_approved_pr_review_follow_on")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("review_verdict"))
.and_then(Value::as_str),
Some("changes_requested")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("has_requested_changes"))
.and_then(Value::as_bool),
Some(true)
);
}
#[tokio::test]
async fn coder_merge_submit_uses_latest_completed_sibling_pr_review() {
let (endpoint, server) = spawn_fake_github_mcp_server().await;
let state = test_state().await;
state
.mcp
.add_or_update(
"github".to_string(),
endpoint,
std::collections::HashMap::new(),
true,
)
.await;
assert!(state.mcp.connect("github").await);
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-submit-latest-review-parent",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 318
},
"mcp_servers": ["github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-parent/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add startup fallback for nil config handling.",
"root_cause": "Missing fallback in recovery path.",
"fix_strategy": "Restore fallback and add regression coverage.",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup fallback regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let draft_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-parent/pr-draft")
.header("content-type", "application/json")
.body(Body::from(json!({}).to_string()))
.expect("draft request");
let draft_resp = app
.clone()
.oneshot(draft_req)
.await
.expect("draft response");
assert_eq!(draft_resp.status(), StatusCode::OK);
let submit_pr_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-parent/pr-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Open the draft PR",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit pr request");
let submit_pr_resp = app
.clone()
.oneshot(submit_pr_req)
.await
.expect("submit pr response");
assert_eq!(submit_pr_resp.status(), StatusCode::OK);
let first_review_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "pr_review",
"coder_run_id": "coder-merge-submit-latest-review-approve"
})
.to_string(),
))
.expect("first review follow-on request");
let first_review_follow_on_resp = app
.clone()
.oneshot(first_review_follow_on_req)
.await
.expect("first review follow-on response");
assert_eq!(first_review_follow_on_resp.status(), StatusCode::OK);
let first_review_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-approve/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "approve",
"summary": "Looks good to merge from the first pass.",
"risk_level": "low",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": [],
"requested_changes": [],
"regression_signals": []
})
.to_string(),
))
.expect("first review summary request");
let first_review_summary_resp = app
.clone()
.oneshot(first_review_summary_req)
.await
.expect("first review summary response");
assert_eq!(first_review_summary_resp.status(), StatusCode::OK);
let second_review_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "pr_review",
"coder_run_id": "coder-merge-submit-latest-review-block"
})
.to_string(),
))
.expect("second review follow-on request");
let second_review_follow_on_resp = app
.clone()
.oneshot(second_review_follow_on_req)
.await
.expect("second review follow-on response");
assert_eq!(second_review_follow_on_resp.status(), StatusCode::OK);
let second_review_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-block/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "A newer review found rollback coverage gaps.",
"risk_level": "medium",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"blockers": [],
"requested_changes": ["Add rollback coverage"],
"regression_signals": []
})
.to_string(),
))
.expect("second review summary request");
let second_review_summary_resp = app
.clone()
.oneshot(second_review_summary_req)
.await
.expect("second review summary response");
assert_eq!(second_review_summary_resp.status(), StatusCode::OK);
let merge_follow_on_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-parent/follow-on-run")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "merge_recommendation",
"coder_run_id": "coder-merge-submit-latest-review-merge"
})
.to_string(),
))
.expect("merge follow-on request");
let merge_follow_on_resp = app
.clone()
.oneshot(merge_follow_on_req)
.await
.expect("merge follow-on response");
assert_eq!(merge_follow_on_resp.status(), StatusCode::OK);
let merge_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-merge/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "Everything looks ready from the merge side.",
"blockers": [],
"required_checks": [],
"required_approvals": []
})
.to_string(),
))
.expect("merge summary request");
let merge_summary_resp = app
.clone()
.oneshot(merge_summary_req)
.await
.expect("merge summary response");
assert_eq!(merge_summary_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-merge/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "Operator approved merge execution."
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let submit_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-submit-latest-review-merge/merge-submit")
.header("content-type", "application/json")
.body(Body::from(
json!({
"approved_by": "user123",
"reason": "Try to merge using the older approval",
"dry_run": false,
"mcp_server": "github"
})
.to_string(),
))
.expect("submit request");
let submit_resp = app
.clone()
.oneshot(submit_req)
.await
.expect("submit response");
server.abort();
assert_eq!(submit_resp.status(), StatusCode::OK);
let submit_payload: Value = serde_json::from_slice(
&to_bytes(submit_resp.into_body(), usize::MAX)
.await
.expect("submit body"),
)
.expect("submit json");
assert_eq!(
submit_payload.get("ok").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
submit_payload.get("code").and_then(Value::as_str),
Some("CODER_MERGE_SUBMIT_POLICY_BLOCKED")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("requires_approved_pr_review_follow_on")
);
assert_eq!(
submit_payload
.get("policy")
.and_then(|row| row.get("review_verdict"))
.and_then(Value::as_str),
Some("changes_requested")
);
}
#[tokio::test]
async fn coder_merge_recommendation_reuses_prior_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_baseline_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-recommendation-baseline",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 93
}
})
.to_string(),
))
.expect("baseline create request");
let create_baseline_resp = app
.clone()
.oneshot(create_baseline_req)
.await
.expect("baseline create response");
assert_eq!(create_baseline_resp.status(), StatusCode::OK);
let baseline_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-recommendation-baseline/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "Hold merge pending final manual verification."
})
.to_string(),
))
.expect("baseline summary request");
let baseline_summary_resp = app
.clone()
.oneshot(baseline_summary_req)
.await
.expect("baseline summary response");
assert_eq!(baseline_summary_resp.status(), StatusCode::OK);
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-recommendation-a",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 93
}
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-recommendation-a/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "Hold merge until the final approval lands and the rollout note is attached.",
"risk_level": "medium",
"blockers": ["Required reviewer approval missing"],
"required_checks": ["ci / test"],
"required_approvals": ["codeowners"],
"memory_hits_used": ["memory-hit-merge-a"]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-recommendation-b",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 93
}
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-recommendation-b/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload.get("query").and_then(Value::as_str),
Some(
"user123/tandem pull request #93 merge recommendation regressions blockers required checks approvals"
)
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("merge_recommendation_memory")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("source_coder_run_id"))
.and_then(Value::as_str)
.or_else(|| {
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("run_id"))
.and_then(Value::as_str)
}),
Some("coder-merge-recommendation-a")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("same_ref"))
.and_then(Value::as_bool),
Some(true)
);
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("kind").and_then(Value::as_str) == Some("merge_recommendation_memory")
&& (row.get("source_coder_run_id").and_then(Value::as_str)
== Some("coder-merge-recommendation-a")
|| row.get("run_id").and_then(Value::as_str)
== Some("coder-merge-recommendation-a"))
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_run_approve_and_cancel_project_context_run_controls() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-controls",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 15
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_body = to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body");
let create_payload: Value = serde_json::from_slice(&create_body).expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run")
.to_string();
let plan_req = Request::builder()
.method("POST")
.uri(format!("/context/runs/{linked_context_run_id}/events"))
.header("content-type", "application/json")
.body(Body::from(
json!({
"type": "planning_started",
"status": "awaiting_approval",
"payload": {}
})
.to_string(),
))
.expect("plan request");
let plan_resp = app.clone().oneshot(plan_req).await.expect("plan response");
assert_eq!(plan_resp.status(), StatusCode::OK);
let approve_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-controls/approve")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "approve coder plan"
})
.to_string(),
))
.expect("approve request");
let approve_resp = app
.clone()
.oneshot(approve_req)
.await
.expect("approve response");
assert_eq!(approve_resp.status(), StatusCode::OK);
let approve_body = to_bytes(approve_resp.into_body(), usize::MAX)
.await
.expect("approve body");
let approve_payload: Value = serde_json::from_slice(&approve_body).expect("approve json");
assert_eq!(
approve_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("repo_inspection")
);
assert_eq!(
approve_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
let cancel_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-controls/cancel")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "stop this coder run"
})
.to_string(),
))
.expect("cancel request");
let cancel_resp = app
.clone()
.oneshot(cancel_req)
.await
.expect("cancel response");
assert_eq!(cancel_resp.status(), StatusCode::OK);
let cancel_body = to_bytes(cancel_resp.into_body(), usize::MAX)
.await
.expect("cancel body");
let cancel_payload: Value = serde_json::from_slice(&cancel_body).expect("cancel json");
assert_eq!(
cancel_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("cancelled")
);
assert_eq!(
cancel_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("cancelled")
);
assert_eq!(
cancel_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("run_outcome")
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-controls/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_body = to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body");
let candidates_payload: Value =
serde_json::from_slice(&candidates_body).expect("candidates json");
let run_outcome = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("run_outcome"))
})
.expect("run outcome candidate");
assert_eq!(
run_outcome
.get("payload")
.and_then(|row| row.get("result"))
.and_then(Value::as_str),
Some("cancelled")
);
assert_eq!(
run_outcome
.get("payload")
.and_then(|row| row.get("reason"))
.and_then(Value::as_str),
Some("stop this coder run")
);
}
#[tokio::test]
async fn coder_issue_triage_run_replay_matches_persisted_state_and_checkpoint() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let (_create_payload, linked_context_run_id) = create_coder_run_for_replay(
app.clone(),
json!({
"coder_run_id": "coder-run-replay",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "issue",
"number": 404,
"url": "https://github.com/user123/tandem/issues/404"
}
}),
)
.await;
let replay_payload = checkpoint_and_replay_coder_run(app.clone(), &linked_context_run_id).await;
assert_eq!(
replay_payload
.get("drift")
.and_then(|row| row.get("mismatch"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
replay_payload
.get("from_checkpoint")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
replay_payload
.get("replay")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_issue_triage")
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("artifacts"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
})),
Some(true)
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(5)
);
}
#[tokio::test]
async fn coder_issue_fix_run_replay_matches_persisted_state_and_checkpoint() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let (_create_payload, linked_context_run_id) = create_coder_run_for_replay(
app.clone(),
json!({
"coder_run_id": "coder-run-fix-replay",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "issue",
"number": 405,
"url": "https://github.com/user123/tandem/issues/405"
}
}),
)
.await;
let replay_payload = checkpoint_and_replay_coder_run(app.clone(), &linked_context_run_id).await;
assert_eq!(
replay_payload
.get("drift")
.and_then(|row| row.get("mismatch"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
replay_payload
.get("from_checkpoint")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
replay_payload
.get("replay")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_issue_fix")
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("artifacts"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
})),
Some(true)
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("implement_patch")
})),
Some(true)
);
}
#[tokio::test]
async fn coder_pr_review_run_replay_matches_persisted_state_and_checkpoint() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let (_create_payload, linked_context_run_id) = create_coder_run_for_replay(
app.clone(),
json!({
"coder_run_id": "coder-run-review-replay",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 406,
"url": "https://github.com/user123/tandem/pull/406"
}
}),
)
.await;
let replay_payload = checkpoint_and_replay_coder_run(app.clone(), &linked_context_run_id).await;
assert_eq!(
replay_payload
.get("drift")
.and_then(|row| row.get("mismatch"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
replay_payload
.get("from_checkpoint")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
replay_payload
.get("replay")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_pr_review")
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("artifacts"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
})),
Some(true)
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str) == Some("review_pull_request")
})),
Some(true)
);
}
#[tokio::test]
async fn coder_merge_recommendation_run_replay_matches_persisted_state_and_checkpoint() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let (_create_payload, linked_context_run_id) = create_coder_run_for_replay(
app.clone(),
json!({
"coder_run_id": "coder-run-merge-replay",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "pull_request",
"number": 407,
"url": "https://github.com/user123/tandem/pull/407"
}
}),
)
.await;
let replay_payload = checkpoint_and_replay_coder_run(app.clone(), &linked_context_run_id).await;
assert_eq!(
replay_payload
.get("drift")
.and_then(|row| row.get("mismatch"))
.and_then(Value::as_bool),
Some(false)
);
assert_eq!(
replay_payload
.get("from_checkpoint")
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
replay_payload
.get("replay")
.and_then(|row| row.get("run_type"))
.and_then(Value::as_str),
Some("coder_merge_recommendation")
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("artifacts"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
})),
Some(true)
);
assert_eq!(
replay_payload
.get("replay_blackboard")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("workflow_node_id").and_then(Value::as_str)
== Some("assess_merge_readiness")
})),
Some(true)
);
}
#[tokio::test]
async fn coder_artifacts_endpoint_projects_context_blackboard_artifacts() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-2",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 9
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let artifacts_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-2/artifacts")
.body(Body::empty())
.expect("artifacts request");
let artifacts_resp = app
.clone()
.oneshot(artifacts_req)
.await
.expect("artifacts response");
assert_eq!(artifacts_resp.status(), StatusCode::OK);
let artifacts_body = to_bytes(artifacts_resp.into_body(), usize::MAX)
.await
.expect("artifacts body");
let artifacts_payload: Value = serde_json::from_slice(&artifacts_body).expect("artifacts json");
let contains_memory_hits = artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
})
})
.unwrap_or(false);
assert!(contains_memory_hits);
}
#[tokio::test]
async fn coder_issue_triage_blocks_when_preferred_mcp_server_is_missing() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state);
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 42
},
"mcp_servers": ["missing-github"]
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::CONFLICT);
let create_body = to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body");
let create_payload: Value = serde_json::from_slice(&create_body).expect("create json");
assert_eq!(
create_payload.get("code").and_then(Value::as_str),
Some("CODER_READINESS_BLOCKED")
);
}
#[tokio::test]
async fn coder_memory_candidate_create_persists_artifact() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-3",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 77
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-3/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "triage_memory",
"summary": "Likely duplicate failure",
"payload": {
"confidence": "medium"
}
})
.to_string(),
))
.expect("candidate request");
let candidate_resp = app
.clone()
.oneshot(candidate_req)
.await
.expect("candidate response");
assert_eq!(candidate_resp.status(), StatusCode::OK);
let artifacts_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-3/artifacts")
.body(Body::empty())
.expect("artifacts request");
let artifacts_resp = app
.clone()
.oneshot(artifacts_req)
.await
.expect("artifacts response");
assert_eq!(artifacts_resp.status(), StatusCode::OK);
let artifacts_body = to_bytes(artifacts_resp.into_body(), usize::MAX)
.await
.expect("artifacts body");
let artifacts_payload: Value = serde_json::from_slice(&artifacts_body).expect("artifacts json");
let contains_candidate = artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_candidate")
})
})
.unwrap_or(false);
assert!(contains_candidate);
}
#[tokio::test]
async fn coder_issue_triage_seeds_ranked_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let first_run_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-seed-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 88
}
})
.to_string(),
))
.expect("first run request");
let first_run_resp = app
.clone()
.oneshot(first_run_req)
.await
.expect("first run response");
assert_eq!(first_run_resp.status(), StatusCode::OK);
let candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-seed-a/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "failure_pattern",
"summary": "Known duplicate failure",
"payload": {
"label": "duplicate"
}
})
.to_string(),
))
.expect("candidate request");
let candidate_resp = app
.clone()
.oneshot(candidate_req)
.await
.expect("candidate response");
assert_eq!(candidate_resp.status(), StatusCode::OK);
let second_run_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-seed-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 88
}
})
.to_string(),
))
.expect("second run request");
let second_run_resp = app
.clone()
.oneshot(second_run_req)
.await
.expect("second run response");
assert_eq!(second_run_resp.status(), StatusCode::OK);
let get_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-seed-b")
.body(Body::empty())
.expect("get request");
let get_resp = app.clone().oneshot(get_req).await.expect("get response");
assert_eq!(get_resp.status(), StatusCode::OK);
let get_body = to_bytes(get_resp.into_body(), usize::MAX)
.await
.expect("get body");
let get_payload: Value = serde_json::from_slice(&get_body).expect("get json");
let retrieve_task = get_payload
.get("run")
.and_then(|row| row.get("tasks"))
.and_then(Value::as_array)
.and_then(|tasks| {
tasks.iter().find(|task| {
task.get("workflow_node_id").and_then(Value::as_str) == Some("retrieve_memory")
})
})
.cloned()
.expect("retrieve task");
let hint_count = retrieve_task
.get("payload")
.and_then(|row| row.get("memory_hits"))
.and_then(Value::as_array)
.map(|rows| rows.len())
.unwrap_or(0);
assert!(hint_count >= 1);
let duplicate_count = retrieve_task
.get("payload")
.and_then(|row| row.get("duplicate_candidates"))
.and_then(Value::as_array)
.map(|rows| rows.len())
.unwrap_or(0);
assert!(duplicate_count >= 1);
}
#[tokio::test]
async fn coder_triage_reproduction_report_advances_triage_run() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-repro",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 96
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let repro_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-repro/triage-reproduction-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Reproduced the capability-readiness issue when bindings are missing.",
"outcome": "reproduced",
"steps": [
"Disconnect GitHub MCP bindings",
"Create issue_triage coder run"
],
"observed_logs": [
"capabilities readiness failed closed"
],
"affected_files": ["crates/tandem-server/src/http/coder.rs"],
"memory_hits_used": ["memory-hit-triage-repro-1"]
})
.to_string(),
))
.expect("repro request");
let repro_resp = app
.clone()
.oneshot(repro_req)
.await
.expect("repro response");
assert_eq!(repro_resp.status(), StatusCode::OK);
let repro_payload: Value = serde_json::from_slice(
&to_bytes(repro_resp.into_body(), usize::MAX)
.await
.expect("repro body"),
)
.expect("repro json");
assert_eq!(
repro_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_reproduction_report")
);
assert_eq!(
repro_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
assert_eq!(
repro_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("artifact_write")
);
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Running);
for workflow_node_id in ["inspect_repo", "attempt_reproduction"] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("write_triage_artifact"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Runnable)
);
}
#[tokio::test]
async fn coder_triage_reproduction_failed_writes_run_outcome_candidate() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-repro-failed",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 196
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let repro_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-repro-failed/triage-reproduction-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"outcome": "failed_to_reproduce",
"steps": [
"Run issue triage with missing runtime condition",
"Observe no deterministic reproduction"
],
"observed_logs": [
"capability readiness blocked execution"
],
"memory_hits_used": ["memory-hit-triage-failure-1"],
"notes": "Preserve this failure outcome for future triage ranking."
})
.to_string(),
))
.expect("repro request");
let repro_resp = app
.clone()
.oneshot(repro_req)
.await
.expect("repro response");
assert_eq!(repro_resp.status(), StatusCode::OK);
let repro_payload: Value = serde_json::from_slice(
&to_bytes(repro_resp.into_body(), usize::MAX)
.await
.expect("repro body"),
)
.expect("repro json");
assert_eq!(
repro_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| row.get("kind").and_then(Value::as_str) == Some("run_outcome"))),
Some(true)
);
assert_eq!(
repro_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| row.get("kind").and_then(Value::as_str) == Some("regression_signal"))),
Some(true)
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-triage-repro-failed/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let run_outcome_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("run_outcome"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("run outcome payload");
assert_eq!(
run_outcome_payload.get("result").and_then(Value::as_str),
Some("triage_reproduction_failed")
);
assert_eq!(
run_outcome_payload
.get("reproduction")
.and_then(|row| row.get("outcome"))
.and_then(Value::as_str),
Some("failed_to_reproduce")
);
let regression_signal_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("regression_signal"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("regression signal payload");
assert_eq!(
regression_signal_payload
.get("result")
.and_then(Value::as_str),
Some("triage_reproduction_failed")
);
assert_eq!(
regression_signal_payload
.get("regression_signals")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("triage_reproduction_failed")
);
}
#[tokio::test]
async fn coder_triage_reproduction_report_infers_memory_and_prior_runs() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_seed_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-repro-seed",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 296
}
})
.to_string(),
))
.expect("seed create request");
let create_seed_resp = app
.clone()
.oneshot(create_seed_req)
.await
.expect("seed create response");
assert_eq!(create_seed_resp.status(), StatusCode::OK);
let seed_candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-repro-seed/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "failure_pattern",
"task_id": "attempt_reproduction",
"summary": "Prior startup recovery failure signature for reproduction context.",
"payload": {
"workflow_mode": "issue_triage",
"summary": "Prior startup recovery failure signature for reproduction context.",
"fingerprint": "triage-repro-seed-fingerprint",
"canonical_markers": ["startup recovery", "repro signal"],
"linked_issue_numbers": [296]
}
})
.to_string(),
))
.expect("seed candidate request");
let seed_candidate_resp = app
.clone()
.oneshot(seed_candidate_req)
.await
.expect("seed candidate response");
assert_eq!(seed_candidate_resp.status(), StatusCode::OK);
let seed_candidate_payload: Value = serde_json::from_slice(
&to_bytes(seed_candidate_resp.into_body(), usize::MAX)
.await
.expect("seed candidate body"),
)
.expect("seed candidate json");
let seeded_candidate_id = seed_candidate_payload
.get("candidate_id")
.and_then(Value::as_str)
.expect("seeded candidate id")
.to_string();
let create_target_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-repro-inferred",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 296
}
})
.to_string(),
))
.expect("target create request");
let create_target_resp = app
.clone()
.oneshot(create_target_req)
.await
.expect("target create response");
assert_eq!(create_target_resp.status(), StatusCode::OK);
let repro_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-repro-inferred/triage-reproduction-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Reproduction report without explicit memory hit ids.",
"outcome": "reproduced",
"steps": [
"Run triage execution path",
"Inspect previous failure markers first"
],
"observed_logs": [
"reused prior startup recovery signal"
]
})
.to_string(),
))
.expect("repro request");
let repro_resp = app
.clone()
.oneshot(repro_req)
.await
.expect("repro response");
assert_eq!(repro_resp.status(), StatusCode::OK);
let repro_payload: Value = serde_json::from_slice(
&to_bytes(repro_resp.into_body(), usize::MAX)
.await
.expect("repro body"),
)
.expect("repro json");
let repro_artifact_path = repro_payload
.get("artifact")
.and_then(|row| row.get("path"))
.and_then(Value::as_str)
.expect("repro artifact path");
let repro_artifact_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(repro_artifact_path)
.await
.expect("read repro artifact"),
)
.expect("parse repro artifact");
assert_eq!(
repro_artifact_payload
.get("memory_hits_used")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| row.as_str() == Some(seeded_candidate_id.as_str()))),
Some(true)
);
assert_eq!(
repro_artifact_payload
.get("prior_runs_considered")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("coder_run_id").and_then(Value::as_str)
== Some("coder-triage-repro-seed")
})
}),
Some(true)
);
}
#[tokio::test]
async fn coder_triage_inspection_report_advances_to_reproduction() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-inspection",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 97
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let inspection_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-inspection/triage-inspection-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "The repo inspection points at capability readiness and MCP binding setup.",
"likely_areas": ["capability resolver", "github readiness"],
"affected_files": ["crates/tandem-server/src/http/coder.rs"],
"memory_hits_used": ["memory-hit-triage-inspection-1"],
"notes": "Inspection completed before reproduction."
})
.to_string(),
))
.expect("inspection request");
let inspection_resp = app
.clone()
.oneshot(inspection_req)
.await
.expect("inspection response");
assert_eq!(inspection_resp.status(), StatusCode::OK);
let inspection_payload: Value = serde_json::from_slice(
&to_bytes(inspection_resp.into_body(), usize::MAX)
.await
.expect("inspection body"),
)
.expect("inspection json");
assert_eq!(
inspection_payload
.get("artifact")
.and_then(|row| row.get("artifact_type"))
.and_then(Value::as_str),
Some("coder_repo_inspection_report")
);
assert_eq!(
inspection_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("running")
);
assert_eq!(
inspection_payload
.get("coder_run")
.and_then(|row| row.get("phase"))
.and_then(Value::as_str),
Some("reproduction")
);
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Running);
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("inspect_repo"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done)
);
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("attempt_reproduction"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Runnable)
);
}
#[tokio::test]
async fn coder_triage_summary_infers_duplicate_linkage_from_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let seed_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-duplicate-linkage-seed",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 512
}
})
.to_string(),
))
.expect("seed request");
let seed_resp = app.clone().oneshot(seed_req).await.expect("seed response");
assert_eq!(seed_resp.status(), StatusCode::OK);
let seed_candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-duplicate-linkage-seed/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "duplicate_linkage",
"task_id": "retrieve_memory",
"summary": "user123/tandem issue #512 is already linked to pull request #913",
"payload": {
"type": "duplicate.issue_pr_linkage",
"repo_slug": "user123/tandem",
"project_id": "proj-engine",
"summary": "user123/tandem issue #512 is already linked to pull request #913",
"linked_issue_numbers": [512],
"linked_pr_numbers": [913],
"relationship": "historical_duplicate_linkage",
"artifact_refs": ["artifacts/pr_submission.json"]
}
})
.to_string(),
))
.expect("seed candidate request");
let seed_candidate_resp = app
.clone()
.oneshot(seed_candidate_req)
.await
.expect("seed candidate response");
assert_eq!(seed_candidate_resp.status(), StatusCode::OK);
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-duplicate-linkage-target",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 512
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-duplicate-linkage-target/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "This issue is likely already covered by an existing pull request.",
"confidence": "high"
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| row.get("kind").and_then(Value::as_str) == Some("duplicate_linkage"))),
Some(true)
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-triage-duplicate-linkage-target/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let duplicate_linkage = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("duplicate_linkage"))
})
.cloned()
.expect("triage duplicate linkage");
assert_eq!(
duplicate_linkage
.get("payload")
.and_then(|row| row.get("linked_issue_numbers"))
.cloned(),
Some(json!([512]))
);
assert_eq!(
duplicate_linkage
.get("payload")
.and_then(|row| row.get("linked_pr_numbers"))
.cloned(),
Some(json!([913]))
);
assert_eq!(
duplicate_linkage
.get("payload")
.and_then(|row| row.get("relationship"))
.and_then(Value::as_str),
Some("issue_triage_duplicate_inference")
);
}
#[tokio::test]
async fn coder_issue_triage_execute_next_drives_task_runtime_to_completion() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let seed_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-execute-seed",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 197
}
})
.to_string(),
))
.expect("seed request");
let seed_resp = app.clone().oneshot(seed_req).await.expect("seed response");
assert_eq!(seed_resp.status(), StatusCode::OK);
let candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-execute-seed/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "failure_pattern",
"summary": "Known startup recovery duplicate",
"payload": {
"type": "failure.pattern",
"repo_slug": "user123/tandem",
"fingerprint": "triage-execute-duplicate",
"symptoms": ["startup recovery", "issue triage"],
"canonical_markers": ["startup recovery", "issue triage", "user123/tandem issue #198"],
"linked_issue_numbers": [198],
"recurrence_count": 2,
"linked_pr_numbers": [],
"affected_components": ["coder"],
"artifact_refs": ["artifact://ctx/manual/triage.summary.json"]
}
})
.to_string(),
))
.expect("candidate request");
let candidate_resp = app
.clone()
.oneshot(candidate_req)
.await
.expect("candidate response");
assert_eq!(candidate_resp.status(), StatusCode::OK);
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-execute-next",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 198
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
for expected in [
"inspect_repo",
"attempt_reproduction",
"write_triage_artifact",
] {
let execute_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-execute-next/execute-next")
.header("content-type", "application/json")
.body(Body::from(
json!({
"agent_id": "coder_engine_worker_test"
})
.to_string(),
))
.expect("execute request");
let execute_resp = app
.clone()
.oneshot(execute_req)
.await
.expect("execute response");
assert_eq!(execute_resp.status(), StatusCode::OK);
let execute_payload: Value = serde_json::from_slice(
&to_bytes(execute_resp.into_body(), usize::MAX)
.await
.expect("execute body"),
)
.expect("execute json");
assert_eq!(
execute_payload
.get("task")
.and_then(|row| row.get("workflow_node_id"))
.and_then(Value::as_str),
Some(expected)
);
if expected != "inspect_pull_request" {
assert_eq!(
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_run_reference"))
.and_then(Value::as_str),
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session_context_run_id"))
.and_then(Value::as_str)
.or_else(|| {
execute_payload
.get("dispatch_result")
.and_then(|row| row.get("worker_session_id"))
.and_then(Value::as_str)
})
);
}
}
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
let blackboard = load_context_blackboard(&state, &linked_context_run_id);
let triage_summary_path = blackboard
.artifacts
.iter()
.find(|artifact| artifact.artifact_type == "coder_triage_summary")
.map(|artifact| artifact.path.clone())
.expect("triage summary path");
let triage_summary_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(&triage_summary_path)
.await
.expect("read triage summary"),
)
.expect("triage summary json");
assert!(triage_summary_payload
.get("duplicate_candidates")
.and_then(Value::as_array)
.map(|rows| !rows.is_empty())
.unwrap_or(false));
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("inspect_repo"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done)
);
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("attempt_reproduction"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done)
);
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some("write_triage_artifact"))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done)
);
}
#[tokio::test]
async fn coder_memory_hits_endpoint_returns_ranked_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-hits-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 95
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-hits-a/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "triage_memory",
"summary": "Repeated issue near capability readiness",
"payload": {
"tag": "known"
}
})
.to_string(),
))
.expect("candidate request");
let candidate_resp = app
.clone()
.oneshot(candidate_req)
.await
.expect("candidate response");
assert_eq!(candidate_resp.status(), StatusCode::OK);
let failure_pattern_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-hits-a/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "failure_pattern",
"summary": "Capability readiness drift repeatedly blocks issue triage startup.",
"payload": {
"type": "historical_failure_pattern",
"root_cause": "GitHub capability bindings were missing during run bootstrap."
}
})
.to_string(),
))
.expect("failure pattern request");
let failure_pattern_resp = app
.clone()
.oneshot(failure_pattern_req)
.await
.expect("failure pattern response");
assert_eq!(failure_pattern_resp.status(), StatusCode::OK);
let second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-hits-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 95
}
})
.to_string(),
))
.expect("second request");
let second_resp = app
.clone()
.oneshot(second_req)
.await
.expect("second response");
assert_eq!(second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-hits-b/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_body = to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body");
let hits_payload: Value = serde_json::from_slice(&hits_body).expect("hits json");
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| !rows.is_empty())
.unwrap_or(false));
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("failure_pattern")
);
assert_eq!(
hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("same_ref"))
.and_then(Value::as_bool),
Some(true)
);
assert_eq!(
hits_payload
.get("retrieval_policy")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_triage")
);
assert_eq!(
hits_payload
.get("retrieval_policy")
.and_then(|row| row.get("sources"))
.cloned(),
Some(json!([
"repo_memory_candidates",
"project_memory",
"governed_memory"
]))
);
assert_eq!(
hits_payload
.get("retrieval_policy")
.and_then(|row| row.get("prioritized_kinds"))
.cloned(),
Some(json!([
"failure_pattern",
"regression_signal",
"duplicate_linkage",
"triage_memory",
"fix_pattern",
"run_outcome"
]))
);
}
#[tokio::test]
async fn coder_issue_triage_retrieves_governed_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let db = super::super::skills_memory::open_global_memory_db()
.await
.expect("global memory db");
db.put_global_memory_record(&GlobalMemoryRecord {
id: "memory-governed-1".to_string(),
user_id: "desktop_developer_mode".to_string(),
source_type: "solution_capsule".to_string(),
content: "Past triage found capability readiness drift in coder issue triage setup"
.to_string(),
content_hash: String::new(),
run_id: "memory-run-1".to_string(),
session_id: None,
message_id: None,
tool_name: None,
project_tag: Some("proj-engine".to_string()),
channel_tag: None,
host_tag: None,
metadata: Some(json!({
"kind": "triage_memory"
})),
provenance: Some(json!({
"origin_event_type": "memory.put"
})),
redaction_status: "passed".to_string(),
redaction_count: 0,
visibility: "private".to_string(),
demoted: false,
score_boost: 0.0,
created_at_ms: crate::now_ms(),
updated_at_ms: crate::now_ms(),
expires_at_ms: None,
})
.await
.expect("seed governed memory");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-governed-hits",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 202
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-governed-hits/memory-hits?q=capability%20readiness")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_body = to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body");
let hits_payload: Value = serde_json::from_slice(&hits_body).expect("hits json");
let has_governed_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("memory_id").and_then(Value::as_str) == Some("memory-governed-1")
})
})
.unwrap_or(false);
assert!(has_governed_hit);
}
#[tokio::test]
async fn coder_triage_summary_write_adds_summary_artifact() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-summary",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 91
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_payload: Value = serde_json::from_slice(
&to_bytes(create_resp.into_body(), usize::MAX)
.await
.expect("create body"),
)
.expect("create json");
let linked_context_run_id = create_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("linked context run id")
.to_string();
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-summary/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Likely duplicate in capabilities flow",
"confidence": "medium",
"affected_files": ["crates/tandem-server/src/http/coder.rs"],
"prior_runs_considered": [{
"coder_run_id": "coder-run-prior-a",
"linked_context_run_id": "ctx-coder-run-prior-a",
"kind": "failure_pattern",
"tier": "project"
}],
"memory_hits_used": ["memcand-1"]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_body = to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body");
let summary_payload: Value = serde_json::from_slice(&summary_body).expect("summary json");
let generated_candidates = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
assert!(generated_candidates
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("triage_memory") }));
assert!(generated_candidates
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("failure_pattern") }));
assert!(generated_candidates
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("run_outcome") }));
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-summary/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_body = to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body");
let candidates_payload: Value =
serde_json::from_slice(&candidates_body).expect("candidates json");
let failure_pattern_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("failure_pattern"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("failure pattern payload");
assert_eq!(
failure_pattern_payload.get("type").and_then(Value::as_str),
Some("failure.pattern")
);
assert_eq!(
failure_pattern_payload
.get("linked_issue_numbers")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(Value::as_u64),
Some(91)
);
let triage_memory_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("triage_memory"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("triage memory payload");
assert_eq!(
triage_memory_payload
.get("prior_runs_considered")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str),
Some("coder-run-prior-a")
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows.len()),
Some(3)
);
assert_eq!(
summary_payload
.get("run")
.and_then(|row| row.get("status"))
.and_then(Value::as_str),
Some("completed")
);
let artifacts_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-summary/artifacts")
.body(Body::empty())
.expect("artifacts request");
let artifacts_resp = app
.clone()
.oneshot(artifacts_req)
.await
.expect("artifacts response");
assert_eq!(artifacts_resp.status(), StatusCode::OK);
let artifacts_body = to_bytes(artifacts_resp.into_body(), usize::MAX)
.await
.expect("artifacts body");
let artifacts_payload: Value = serde_json::from_slice(&artifacts_body).expect("artifacts json");
let contains_summary = artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_triage_summary")
})
})
.unwrap_or(false);
assert!(contains_summary);
let summary_path = load_context_blackboard(&state, &linked_context_run_id)
.artifacts
.iter()
.find(|artifact| artifact.artifact_type == "coder_triage_summary")
.map(|artifact| artifact.path.clone())
.expect("triage summary artifact path");
let summary_artifact_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(&summary_path)
.await
.expect("read triage summary artifact"),
)
.expect("triage summary artifact json");
assert_eq!(
summary_artifact_payload
.get("prior_runs_considered")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("coder_run_id"))
.and_then(Value::as_str),
Some("coder-run-prior-a")
);
let contains_memory_hits = artifacts_payload
.get("artifacts")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("artifact_type").and_then(Value::as_str) == Some("coder_memory_hits")
})
})
.unwrap_or(false);
assert!(contains_memory_hits);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-summary/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_body = to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body");
let candidates_payload: Value =
serde_json::from_slice(&candidates_body).expect("candidates json");
let kinds = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.map(|rows| {
rows.iter()
.filter_map(|row| row.get("kind").and_then(Value::as_str))
.collect::<Vec<_>>()
})
.unwrap_or_default();
assert!(kinds.contains(&"triage_memory"));
assert!(kinds.contains(&"run_outcome"));
let run = load_context_run_state(&state, &linked_context_run_id)
.await
.expect("context run state");
assert_eq!(run.status, ContextRunStatus::Completed);
let blackboard = load_context_blackboard(&state, &linked_context_run_id);
assert!(blackboard
.artifacts
.iter()
.any(|artifact| artifact.artifact_type == "coder_triage_summary"));
for workflow_node_id in [
"ingest_reference",
"retrieve_memory",
"inspect_repo",
"attempt_reproduction",
"write_triage_artifact",
] {
assert_eq!(
run.tasks
.iter()
.find(|task| task.workflow_node_id.as_deref() == Some(workflow_node_id))
.map(|task| &task.status),
Some(&ContextBlackboardTaskStatus::Done),
"expected {workflow_node_id} to be done"
);
}
}
#[tokio::test]
async fn coder_triage_summary_writes_run_outcome_without_summary_text() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-triage-outcome-only",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 191
}
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-triage-outcome-only/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"confidence": "low",
"reproduction": {
"outcome": "failed_to_reproduce",
"steps": ["cargo test -p tandem-server missing_case -- --test-threads=1"]
},
"notes": "Issue triage failed before reliable reproduction but should still keep an outcome."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("run_outcome") })),
Some(true)
);
assert_eq!(
summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| { row.get("kind").and_then(Value::as_str) == Some("triage_memory") })),
Some(false)
);
let candidates_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-triage-outcome-only/memory-candidates")
.body(Body::empty())
.expect("candidates request");
let candidates_resp = app
.clone()
.oneshot(candidates_req)
.await
.expect("candidates response");
assert_eq!(candidates_resp.status(), StatusCode::OK);
let candidates_payload: Value = serde_json::from_slice(
&to_bytes(candidates_resp.into_body(), usize::MAX)
.await
.expect("candidates body"),
)
.expect("candidates json");
let run_outcome_payload = candidates_payload
.get("candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("run_outcome"))
})
.and_then(|row| row.get("payload"))
.cloned()
.expect("run outcome payload");
assert_eq!(
run_outcome_payload.get("summary").and_then(Value::as_str),
Some("Issue triage reproduction outcome: failed_to_reproduce")
);
}
#[tokio::test]
async fn coder_triage_summary_infers_duplicate_and_memory_fields_from_bootstrap_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_seed_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-triage-seed",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 601
}
})
.to_string(),
))
.expect("seed create request");
let create_seed_resp = app
.clone()
.oneshot(create_seed_req)
.await
.expect("seed create response");
assert_eq!(create_seed_resp.status(), StatusCode::OK);
let seed_candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-triage-seed/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "failure_pattern",
"task_id": "attempt_reproduction",
"summary": "Seeded startup recovery failure pattern",
"payload": {
"workflow_mode": "issue_triage",
"summary": "Seeded startup recovery failure pattern",
"fingerprint": "seeded-startup-recovery-fingerprint",
"canonical_markers": ["startup recovery", "panic"],
"linked_issue_numbers": [601],
"affected_components": ["crates/tandem-server/src/http/coder.rs"]
}
})
.to_string(),
))
.expect("seed candidate request");
let seed_candidate_resp = app
.clone()
.oneshot(seed_candidate_req)
.await
.expect("seed candidate response");
assert_eq!(seed_candidate_resp.status(), StatusCode::OK);
let seed_candidate_payload: Value = serde_json::from_slice(
&to_bytes(seed_candidate_resp.into_body(), usize::MAX)
.await
.expect("seed candidate body"),
)
.expect("seed candidate json");
let seeded_candidate_id = seed_candidate_payload
.get("candidate_id")
.and_then(Value::as_str)
.expect("seeded candidate id")
.to_string();
let create_target_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-triage-auto-fields",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 601
}
})
.to_string(),
))
.expect("target create request");
let create_target_resp = app
.clone()
.oneshot(create_target_req)
.await
.expect("target create response");
assert_eq!(create_target_resp.status(), StatusCode::OK);
let create_target_payload: Value = serde_json::from_slice(
&to_bytes(create_target_resp.into_body(), usize::MAX)
.await
.expect("target create body"),
)
.expect("target create json");
let target_context_run_id = create_target_payload
.get("coder_run")
.and_then(|row| row.get("linked_context_run_id"))
.and_then(Value::as_str)
.expect("target linked context run id")
.to_string();
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-triage-auto-fields/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Automatically infer duplicate and memory provenance fields.",
"confidence": "high"
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let triage_summary_path = load_context_blackboard(&state, &target_context_run_id)
.artifacts
.iter()
.find(|artifact| artifact.artifact_type == "coder_triage_summary")
.map(|artifact| artifact.path.clone())
.expect("triage summary artifact path");
let triage_summary_payload: Value = serde_json::from_str(
&tokio::fs::read_to_string(&triage_summary_path)
.await
.expect("read triage summary artifact"),
)
.expect("parse triage summary artifact");
assert_eq!(
triage_summary_payload
.get("memory_hits_used")
.and_then(Value::as_array)
.map(|rows| rows
.iter()
.any(|row| row.as_str() == Some(seeded_candidate_id.as_str()))),
Some(true)
);
assert_eq!(
triage_summary_payload
.get("prior_runs_considered")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("coder_run_id").and_then(Value::as_str) == Some("coder-run-triage-seed")
})
}),
Some(true)
);
assert_eq!(
triage_summary_payload
.get("duplicate_candidates")
.and_then(Value::as_array)
.map(|rows| !rows.is_empty()),
Some(true)
);
}
#[tokio::test]
async fn coder_memory_candidate_promote_stores_governed_memory() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-promote",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 333
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-promote/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Capability readiness drift already explained this failure",
"confidence": "high"
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_body = to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body");
let summary_payload: Value = serde_json::from_slice(&summary_body).expect("summary json");
let triage_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("triage_memory")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("triage candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-run-promote/memory-candidates/{triage_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable triage memory"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_body = to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body");
let promote_payload: Value = serde_json::from_slice(&promote_body).expect("promote json");
assert_eq!(
promote_payload.get("promoted").and_then(Value::as_bool),
Some(true)
);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-promote/memory-hits?q=capability%20readiness")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_body = to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body");
let hits_payload: Value = serde_json::from_slice(&hits_body).expect("hits json");
let has_promoted_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("memory_id").and_then(Value::as_str)
== promote_payload.get("memory_id").and_then(Value::as_str)
})
})
.unwrap_or(false);
assert!(has_promoted_hit);
let create_fix_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-promote-fix",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 334
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create fix request");
let create_fix_resp = app
.clone()
.oneshot(create_fix_req)
.await
.expect("create fix response");
assert_eq!(create_fix_resp.status(), StatusCode::OK);
let fix_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-promote-fix/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add the missing startup fallback guard and validate recovery behavior.",
"root_cause": "Startup recovery skipped the nil-config fallback path.",
"fix_strategy": "add startup fallback guard",
"changed_files": ["crates/tandem-server/src/http/coder.rs"]
})
.to_string(),
))
.expect("fix summary request");
let fix_summary_resp = app
.clone()
.oneshot(fix_summary_req)
.await
.expect("fix summary response");
assert_eq!(fix_summary_resp.status(), StatusCode::OK);
let fix_summary_payload: Value = serde_json::from_slice(
&to_bytes(fix_summary_resp.into_body(), usize::MAX)
.await
.expect("fix summary body"),
)
.expect("fix summary json");
let fix_pattern_candidate_id = fix_summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("fix_pattern")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("fix pattern candidate id");
let promote_fix_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-run-promote-fix/memory-candidates/{fix_pattern_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable fix pattern"
})
.to_string(),
))
.expect("promote fix request");
let promote_fix_resp = app
.clone()
.oneshot(promote_fix_req)
.await
.expect("promote fix response");
assert_eq!(promote_fix_resp.status(), StatusCode::OK);
let promote_fix_payload: Value = serde_json::from_slice(
&to_bytes(promote_fix_resp.into_body(), usize::MAX)
.await
.expect("promote fix body"),
)
.expect("promote fix json");
assert_eq!(
promote_fix_payload.get("promoted").and_then(Value::as_bool),
Some(true)
);
let fix_hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-run-promote-fix/memory-hits?q=startup%20fallback%20guard")
.body(Body::empty())
.expect("fix hits request");
let fix_hits_resp = app
.clone()
.oneshot(fix_hits_req)
.await
.expect("fix hits response");
assert_eq!(fix_hits_resp.status(), StatusCode::OK);
let fix_hits_payload: Value = serde_json::from_slice(
&to_bytes(fix_hits_resp.into_body(), usize::MAX)
.await
.expect("fix hits body"),
)
.expect("fix hits json");
let has_promoted_fix_hit = fix_hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| {
rows.iter().any(|row| {
row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("memory_id").and_then(Value::as_str)
== promote_fix_payload.get("memory_id").and_then(Value::as_str)
&& row
.get("metadata")
.and_then(|metadata| metadata.get("kind"))
.and_then(Value::as_str)
== Some("fix_pattern")
})
})
.unwrap_or(false);
assert!(has_promoted_fix_hit);
let db = super::super::skills_memory::open_global_memory_db()
.await
.expect("global memory db");
let promoted_fix_record = db
.get_global_memory(
promote_fix_payload
.get("memory_id")
.and_then(Value::as_str)
.expect("fix memory id"),
)
.await
.expect("load fix governed memory")
.expect("fix governed memory record");
assert_eq!(promoted_fix_record.source_type, "solution_capsule");
assert_eq!(
promoted_fix_record.project_tag.as_deref(),
Some("proj-engine")
);
assert!(promoted_fix_record.content.contains("workflow: issue_fix"));
assert!(promoted_fix_record
.content
.contains("fix_strategy: add startup fallback guard"));
assert!(promoted_fix_record
.content
.contains("root_cause: Startup recovery skipped the nil-config fallback path."));
assert_eq!(
promoted_fix_record
.metadata
.as_ref()
.and_then(|metadata| metadata.get("kind"))
.and_then(Value::as_str),
Some("fix_pattern")
);
assert_eq!(
promoted_fix_record
.metadata
.as_ref()
.and_then(|metadata| metadata.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_fix")
);
assert_eq!(
promoted_fix_record
.metadata
.as_ref()
.and_then(|metadata| metadata.get("candidate_id"))
.and_then(Value::as_str),
Some(fix_pattern_candidate_id.as_str())
);
}
#[tokio::test]
async fn coder_issue_triage_reuses_promoted_fix_pattern_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_fix_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-fix-history-a",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "issue",
"number": 96,
"url": "https://github.com/user123/tandem/issues/96"
}
})
.to_string(),
))
.expect("create fix request");
let create_fix_resp = app
.clone()
.oneshot(create_fix_req)
.await
.expect("create fix response");
assert_eq!(create_fix_resp.status(), StatusCode::OK);
let fix_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-fix-history-a/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add the startup fallback guard and keep the service booting when config is absent.",
"root_cause": "Startup recovery skipped the nil-config fallback path.",
"fix_strategy": "add startup fallback guard",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup fallback regression stays covered"
}]
})
.to_string(),
))
.expect("fix summary request");
let fix_summary_resp = app
.clone()
.oneshot(fix_summary_req)
.await
.expect("fix summary response");
assert_eq!(fix_summary_resp.status(), StatusCode::OK);
let fix_summary_payload: Value = serde_json::from_slice(
&to_bytes(fix_summary_resp.into_body(), usize::MAX)
.await
.expect("fix summary body"),
)
.expect("fix summary json");
let fix_pattern_candidate_id = fix_summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("fix_pattern")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("fix pattern candidate id");
let promote_fix_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-triage-fix-history-a/memory-candidates/{fix_pattern_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-96",
"reason": "approved reusable fix pattern for future triage"
})
.to_string(),
))
.expect("promote fix request");
let promote_fix_resp = app
.clone()
.oneshot(promote_fix_req)
.await
.expect("promote fix response");
assert_eq!(promote_fix_resp.status(), StatusCode::OK);
let promote_fix_payload: Value = serde_json::from_slice(
&to_bytes(promote_fix_resp.into_body(), usize::MAX)
.await
.expect("promote fix body"),
)
.expect("promote fix json");
let create_triage_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-fix-history-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem",
"default_branch": "main"
},
"github_ref": {
"kind": "issue",
"number": 96,
"url": "https://github.com/user123/tandem/issues/96"
}
})
.to_string(),
))
.expect("create triage request");
let create_triage_resp = app
.clone()
.oneshot(create_triage_req)
.await
.expect("create triage response");
assert_eq!(create_triage_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-triage-fix-history-b/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload.get("query").and_then(Value::as_str),
Some("user123/tandem issue #96")
);
assert!(hits_payload
.get("policy")
.and_then(|row| row.get("prioritized_kinds"))
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| row.as_str() == Some("fix_pattern")))
.unwrap_or(false));
assert!(hits_payload
.get("hits")
.and_then(Value::as_array)
.map(|rows| rows.iter().any(|row| {
row.get("memory_id").and_then(Value::as_str)
== promote_fix_payload.get("memory_id").and_then(Value::as_str)
&& row.get("kind").and_then(Value::as_str) == Some("fix_pattern")
&& row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("same_issue").and_then(Value::as_bool) == Some(true)
}))
.unwrap_or(false));
}
#[tokio::test]
async fn coder_promoted_merge_memory_reuses_policy_history_across_pull_requests() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-promote-a",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 101
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-promote-a/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "Hold merge until ci / test passes and codeowners approval lands.",
"risk_level": "medium",
"blockers": ["Required reviewer approval missing"],
"required_checks": ["ci / test"],
"required_approvals": ["codeowners"]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let merge_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("merge_recommendation_memory"))
.then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("merge candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-merge-promote-a/memory-candidates/{merge_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable merge policy memory"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-promote-b",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 102
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-promote-b/memory-hits?q=codeowners%20ci%20%2F%20test%20approval")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let promoted_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find(|row| {
row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("memory_id").and_then(Value::as_str)
== promote_payload.get("memory_id").and_then(Value::as_str)
})
})
.cloned()
.expect("promoted merge hit");
assert_eq!(promoted_hit.get("same_ref").and_then(Value::as_bool), None);
assert_eq!(
promoted_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("merge_recommendation_memory")
);
assert!(promoted_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("required_checks: ci / test")));
assert!(promoted_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("required_approvals: codeowners")));
assert!(promoted_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("blockers: Required reviewer approval missing")));
}
#[tokio::test]
async fn coder_merge_recommendation_memory_promotion_requires_policy_signals() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-promote-policy-guard",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 141
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-promote-policy-guard/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "merge",
"summary": "All signals pass; merge can proceed."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let merge_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("merge_recommendation_memory"))
.then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("merge recommendation candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-merge-promote-policy-guard/memory-candidates/{merge_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "attempted promotion without policy context"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn coder_promoted_merge_outcome_reuses_across_pull_requests() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-outcome-promote-a",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 111
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-outcome-promote-a/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "Merge should wait until rollout notes are attached and post-deploy verification is ready.",
"risk_level": "medium"
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let run_outcome_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("run_outcome")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("run outcome candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-merge-outcome-promote-a/memory-candidates/{run_outcome_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable merge outcome"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-outcome-promote-b",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 112
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-outcome-promote-b/memory-hits?q=merge%20should%20wait%20until%20rollout%20notes%20are%20attached")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("run_outcome")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("merge_recommendation")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
}
#[tokio::test]
async fn coder_duplicate_linkage_promotion_requires_linked_issue_and_pr() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-duplicate-linkage-guard",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 991
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-duplicate-linkage-guard/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "duplicate_linkage",
"summary": "Link issue to follow-on PR",
"payload": {
"linked_issue_numbers": [991]
}
})
.to_string(),
))
.expect("candidate request");
let create_candidate_resp = app
.clone()
.oneshot(create_candidate_req)
.await
.expect("candidate response");
assert_eq!(create_candidate_resp.status(), StatusCode::OK);
let create_candidate_payload: Value = serde_json::from_slice(
&to_bytes(create_candidate_resp.into_body(), usize::MAX)
.await
.expect("candidate body"),
)
.expect("candidate json");
let candidate_id = create_candidate_payload
.get("candidate_id")
.and_then(Value::as_str)
.expect("candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-duplicate-linkage-guard/memory-candidates/{candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "attempted reusable duplicate linkage"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn coder_regression_signal_promotion_requires_structured_signals() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-regression-guard",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 992
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let create_candidate_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-regression-guard/memory-candidates")
.header("content-type", "application/json")
.body(Body::from(
json!({
"kind": "regression_signal",
"summary": "Historical deploy regression repeated",
"payload": {
"workflow_mode": "pr_review",
"summary_artifact_path": "/tmp/fake-summary.json",
"regression_signals": []
}
})
.to_string(),
))
.expect("candidate request");
let create_candidate_resp = app
.clone()
.oneshot(create_candidate_req)
.await
.expect("candidate response");
assert_eq!(create_candidate_resp.status(), StatusCode::OK);
let create_candidate_payload: Value = serde_json::from_slice(
&to_bytes(create_candidate_resp.into_body(), usize::MAX)
.await
.expect("candidate body"),
)
.expect("candidate json");
let candidate_id = create_candidate_payload
.get("candidate_id")
.and_then(Value::as_str)
.expect("candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-regression-guard/memory-candidates/{candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "attempted reusable regression signal"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn coder_terminal_run_outcome_promotion_requires_workflow_evidence() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-outcome-guard",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 993
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let cancel_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-outcome-guard/cancel")
.header("content-type", "application/json")
.body(Body::from(
json!({
"reason": "stop this coder run"
})
.to_string(),
))
.expect("cancel request");
let cancel_resp = app
.clone()
.oneshot(cancel_req)
.await
.expect("cancel response");
assert_eq!(cancel_resp.status(), StatusCode::OK);
let cancel_payload: Value = serde_json::from_slice(
&to_bytes(cancel_resp.into_body(), usize::MAX)
.await
.expect("cancel body"),
)
.expect("cancel json");
let candidate_id = cancel_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("candidate_id"))
.and_then(Value::as_str)
.expect("run outcome candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-run-outcome-guard/memory-candidates/{candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "attempted reusable cancelled outcome"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn coder_pr_review_reuses_prior_merge_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_merge_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-merge-memory-source",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 190
}
})
.to_string(),
))
.expect("create merge request");
let create_merge_resp = app
.clone()
.oneshot(create_merge_req)
.await
.expect("create merge response");
assert_eq!(create_merge_resp.status(), StatusCode::OK);
let merge_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-pr-review-merge-memory-source/merge-recommendation-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"recommendation": "hold",
"summary": "Hold merge until rollout validation completes.",
"blockers": ["Rollout validation still pending"],
"required_checks": ["staging-rollout"]
})
.to_string(),
))
.expect("merge summary request");
let merge_summary_resp = app
.clone()
.oneshot(merge_summary_req)
.await
.expect("merge summary response");
assert_eq!(merge_summary_resp.status(), StatusCode::OK);
let create_review_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-pr-review-merge-memory-target",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 190
}
})
.to_string(),
))
.expect("create review request");
let create_review_resp = app
.clone()
.oneshot(create_review_req)
.await
.expect("create review response");
assert_eq!(create_review_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-pr-review-merge-memory-target/memory-hits?q=rollout%20validation%20pending")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let merge_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find(|row| {
row.get("kind").and_then(Value::as_str) == Some("merge_recommendation_memory")
})
})
.cloned()
.expect("merge recommendation hit");
assert_eq!(
merge_hit.get("same_ref").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
hits_payload
.get("retrieval_policy")
.and_then(|row| row.get("prioritized_kinds"))
.cloned(),
Some(json!([
"review_memory",
"merge_recommendation_memory",
"duplicate_linkage",
"regression_signal",
"run_outcome"
]))
);
}
#[tokio::test]
async fn coder_merge_recommendation_reuses_prior_review_memory_hits() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_review_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-review-memory-source",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 191
}
})
.to_string(),
))
.expect("create review request");
let create_review_resp = app
.clone()
.oneshot(create_review_req)
.await
.expect("create review response");
assert_eq!(create_review_resp.status(), StatusCode::OK);
let review_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-review-memory-source/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "Require rollout approval evidence before merge.",
"requested_changes": ["Attach rollout approval evidence"],
"blockers": ["Approval evidence missing"]
})
.to_string(),
))
.expect("review summary request");
let review_summary_resp = app
.clone()
.oneshot(review_summary_req)
.await
.expect("review summary response");
assert_eq!(review_summary_resp.status(), StatusCode::OK);
let create_merge_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-review-memory-target",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 191
}
})
.to_string(),
))
.expect("create merge request");
let create_merge_resp = app
.clone()
.oneshot(create_merge_req)
.await
.expect("create merge response");
assert_eq!(create_merge_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-review-memory-target/memory-hits?q=approval%20evidence%20missing")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let review_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter()
.find(|row| row.get("kind").and_then(Value::as_str) == Some("review_memory"))
})
.cloned()
.expect("review memory hit");
assert_eq!(
review_hit.get("same_ref").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
hits_payload
.get("retrieval_policy")
.and_then(|row| row.get("prioritized_kinds"))
.cloned(),
Some(json!([
"merge_recommendation_memory",
"review_memory",
"duplicate_linkage",
"run_outcome",
"regression_signal"
]))
);
}
#[tokio::test]
async fn coder_promoted_review_memory_reuses_requested_changes_across_pull_requests() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-review-promote-a",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 111
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-review-promote-a/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "Require rollback coverage before approval.",
"risk_level": "high",
"blockers": ["Rollback scenario coverage missing"],
"requested_changes": ["Add rollback coverage"],
"changed_files": ["crates/tandem-server/src/http/coder.rs"]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let review_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("review_memory")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("review candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-review-promote-a/memory-candidates/{review_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable review guidance"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-review-promote-b",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 112
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-review-promote-b/memory-hits?q=rollback%20coverage")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let promoted_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find(|row| {
row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("memory_id").and_then(Value::as_str)
== promote_payload.get("memory_id").and_then(Value::as_str)
})
})
.cloned()
.expect("promoted review hit");
assert_eq!(
promoted_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("review_memory")
);
assert!(promoted_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("requested_changes: Add rollback coverage")));
assert!(promoted_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("blockers: Rollback scenario coverage missing")));
}
#[tokio::test]
async fn coder_promoted_regression_signal_reuses_across_pull_requests() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-regression-promote-a",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 501
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-regression-promote-a/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "This change repeats the rollback-free migration pattern that regressed previously.",
"regression_signals": [{
"kind": "historical_failure_pattern",
"summary": "Rollback-free migrations regressed previously during deploy."
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let regression_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("regression_signal")).then(
|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
},
)?
})
})
.expect("regression candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-regression-promote-a/memory-candidates/{regression_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable regression signal"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-regression-promote-b",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 502
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-regression-promote-b/memory-hits?q=rollback-free%20migrations%20regressed%20during%20deploy")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("regression_signal")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
}
#[tokio::test]
async fn coder_merge_recommendation_reuses_promoted_regression_signal_across_pull_requests() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_review_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-regression-source",
"workflow_mode": "pr_review",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 601
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create review request");
let create_review_resp = app
.clone()
.oneshot(create_review_req)
.await
.expect("create review response");
assert_eq!(create_review_resp.status(), StatusCode::OK);
let review_summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-merge-regression-source/pr-review-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"verdict": "changes_requested",
"summary": "This PR repeats the rollback-free deploy regression pattern and should not merge yet.",
"regression_signals": [{
"kind": "historical_failure_pattern",
"summary": "Rollback-free deploys regressed previously during release cutovers."
}]
})
.to_string(),
))
.expect("review summary request");
let review_summary_resp = app
.clone()
.oneshot(review_summary_req)
.await
.expect("review summary response");
assert_eq!(review_summary_resp.status(), StatusCode::OK);
let review_summary_payload: Value = serde_json::from_slice(
&to_bytes(review_summary_resp.into_body(), usize::MAX)
.await
.expect("review summary body"),
)
.expect("review summary json");
let regression_candidate_id = review_summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("regression_signal")).then(
|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
},
)?
})
})
.expect("regression candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-merge-regression-source/memory-candidates/{regression_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable merge regression history"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_merge_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-merge-regression-target",
"workflow_mode": "merge_recommendation",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "pull_request",
"number": 602
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create merge request");
let create_merge_resp = app
.clone()
.oneshot(create_merge_req)
.await
.expect("create merge response");
assert_eq!(create_merge_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-merge-regression-target/memory-hits")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
assert_eq!(
hits_payload.get("query").and_then(Value::as_str),
Some(
"user123/tandem pull request #602 merge recommendation regressions blockers required checks approvals"
)
);
let promoted_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find(|row| {
row.get("source").and_then(Value::as_str) == Some("governed_memory")
&& row.get("memory_id").and_then(Value::as_str)
== promote_payload.get("memory_id").and_then(Value::as_str)
})
})
.cloned()
.expect("promoted regression hit");
assert_eq!(
promoted_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("regression_signal")
);
assert_eq!(
promoted_hit
.get("metadata")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("pr_review")
);
assert_eq!(promoted_hit.get("same_ref").and_then(Value::as_bool), None);
assert!(promoted_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("Rollback-free deploys regressed previously")));
}
#[tokio::test]
async fn coder_promoted_fix_memory_reuses_strategy_across_issues() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-fix-promote-a",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 201
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-fix-promote-a/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add the startup fallback guard and cover the nil-config recovery path.",
"root_cause": "Startup recovery skipped the nil-config fallback path.",
"fix_strategy": "add startup fallback guard",
"validation_steps": ["cargo test -p tandem-server coder_promoted_fix_memory_reuses_strategy_across_issues -- --test-threads=1"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup fallback recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let fix_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("fix_pattern")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("fix candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-fix-promote-a/memory-candidates/{fix_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable fix pattern"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-fix-promote-b",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 202
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-fix-promote-b/memory-hits?q=startup%20fallback%20guard%20nil-config%20recovery")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("fix_pattern")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
assert!(first_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("fix_strategy: add startup fallback guard")));
assert!(first_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| {
content.contains("root_cause: Startup recovery skipped the nil-config fallback path.")
}));
}
#[tokio::test]
async fn coder_promoted_validation_memory_reuses_across_issues() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-validation-promote-a",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 211
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-validation-promote-a/issue-fix-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Add the startup fallback guard and verify recovery with a targeted regression.",
"root_cause": "Startup recovery skipped the nil-config fallback path.",
"fix_strategy": "add startup fallback guard",
"validation_steps": ["cargo test -p tandem-server coder_promoted_validation_memory_reuses_across_issues -- --test-threads=1"],
"validation_results": [{
"kind": "test",
"status": "passed",
"summary": "startup recovery regression passed"
}]
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let validation_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("validation_memory")).then(
|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
},
)?
})
})
.expect("validation candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-validation-promote-a/memory-candidates/{validation_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable validation evidence"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-validation-promote-b",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 212
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-validation-promote-b/memory-hits?q=startup%20recovery%20regression%20passed")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("validation_memory")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
assert!(first_hit.get("content").and_then(Value::as_str).is_some());
}
#[tokio::test]
async fn coder_promoted_issue_fix_regression_signal_reuses_across_issues() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-fix-regression-promote-a",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 213
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let validation_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-fix-regression-promote-a/issue-fix-validation-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Guarded the startup recovery path, but the targeted regression still fails.",
"root_cause": "Startup recovery skipped the nil-config fallback path.",
"fix_strategy": "guard fallback branch",
"changed_files": ["crates/tandem-server/src/http/coder.rs"],
"validation_steps": ["cargo test -p tandem-server coder_promoted_issue_fix_regression_signal_reuses_across_issues -- --test-threads=1"],
"validation_results": [{
"kind": "test",
"status": "failed",
"summary": "startup recovery regression still fails"
}]
})
.to_string(),
))
.expect("validation request");
let validation_resp = app
.clone()
.oneshot(validation_req)
.await
.expect("validation response");
assert_eq!(validation_resp.status(), StatusCode::OK);
let validation_payload: Value = serde_json::from_slice(
&to_bytes(validation_resp.into_body(), usize::MAX)
.await
.expect("validation body"),
)
.expect("validation json");
let regression_candidate_id = validation_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("regression_signal")).then(
|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
},
)?
})
})
.expect("regression candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-fix-regression-promote-a/memory-candidates/{regression_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable issue-fix regression signal"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-fix-regression-promote-b",
"workflow_mode": "issue_fix",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 214
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-fix-regression-promote-b/memory-hits?q=startup%20recovery%20regression%20still%20fails")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("regression_signal")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
}
#[tokio::test]
async fn coder_promoted_failure_pattern_reuses_across_issues() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-failure-pattern-promote-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 301
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-failure-pattern-promote-a/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "GitHub capability bindings drifted, so issue triage failed before reproduction.",
"confidence": "high",
"likely_root_cause": "Capability readiness drift in GitHub issue bindings.",
"reproduction": "Run creation halted before reproduction because GitHub issue capabilities were missing."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let failure_pattern_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("failure_pattern")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("failure pattern candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-failure-pattern-promote-a/memory-candidates/{failure_pattern_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable failure pattern"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-failure-pattern-promote-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 302
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-failure-pattern-promote-b/memory-hits?q=github%20capability%20bindings%20drift%20issue%20triage%20reproduction%20missing")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("failure_pattern")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
assert!(first_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("GitHub capability bindings drifted")));
assert!(first_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("issue triage failed before reproduction")));
}
#[tokio::test]
async fn coder_promoted_triage_outcome_reuses_across_issues() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-outcome-promote-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 401
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-outcome-promote-a/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Capability readiness drift in GitHub issue bindings is the likely root cause.",
"confidence": "high",
"likely_root_cause": "GitHub issue bindings were not connected when triage started."
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let run_outcome_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("run_outcome")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("run outcome candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-triage-outcome-promote-a/memory-candidates/{run_outcome_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable triage outcome"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-outcome-promote-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 402
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-triage-outcome-promote-b/memory-hits?q=issue%20triage%20completed%20high%20confidence")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let first_hit = hits_payload
.get("hits")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.cloned()
.expect("first hit");
assert_eq!(
first_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("kind"))
.and_then(Value::as_str),
Some("run_outcome")
);
assert_eq!(
first_hit
.get("metadata")
.and_then(|row| row.get("workflow_mode"))
.and_then(Value::as_str),
Some("issue_triage")
);
assert_eq!(
first_hit.get("memory_id").and_then(Value::as_str),
promote_payload.get("memory_id").and_then(Value::as_str)
);
assert_eq!(first_hit.get("same_ref").and_then(Value::as_bool), None);
assert!(first_hit.get("content").and_then(Value::as_str).is_some());
}
#[tokio::test]
async fn coder_promoted_triage_regression_signal_reuses_across_issues() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let app = app_router(state.clone());
let create_first_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-regression-promote-a",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 451
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("first create request");
let create_first_resp = app
.clone()
.oneshot(create_first_req)
.await
.expect("first create response");
assert_eq!(create_first_resp.status(), StatusCode::OK);
let repro_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-triage-regression-promote-a/triage-reproduction-report")
.header("content-type", "application/json")
.body(Body::from(
json!({
"outcome": "failed_to_reproduce",
"steps": [
"Start the triage workflow against a misconfigured GitHub runtime",
"Observe the same readiness failure before reproduction can complete"
],
"observed_logs": [
"GitHub capability bindings drifted from the expected project setup"
],
"memory_hits_used": ["memory-hit-triage-regression-1"],
"notes": "Keep this regression signal for future issue triage."
})
.to_string(),
))
.expect("repro request");
let repro_resp = app
.clone()
.oneshot(repro_req)
.await
.expect("repro response");
assert_eq!(repro_resp.status(), StatusCode::OK);
let repro_payload: Value = serde_json::from_slice(
&to_bytes(repro_resp.into_body(), usize::MAX)
.await
.expect("repro body"),
)
.expect("repro json");
let regression_signal_candidate_id = repro_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("regression_signal")).then(
|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
},
)?
})
})
.expect("regression signal candidate id");
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-triage-regression-promote-a/memory-candidates/{regression_signal_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable triage regression signal"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promote_payload: Value = serde_json::from_slice(
&to_bytes(promote_resp.into_body(), usize::MAX)
.await
.expect("promote body"),
)
.expect("promote json");
let create_second_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-triage-regression-promote-b",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 452
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("second create request");
let create_second_resp = app
.clone()
.oneshot(create_second_req)
.await
.expect("second create response");
assert_eq!(create_second_resp.status(), StatusCode::OK);
let hits_req = Request::builder()
.method("GET")
.uri("/coder/runs/coder-triage-regression-promote-b/memory-hits?q=Issue%20triage%20regression%20signal%20failed_to_reproduce%20Keep%20this%20regression%20signal")
.body(Body::empty())
.expect("hits request");
let hits_resp = app.clone().oneshot(hits_req).await.expect("hits response");
assert_eq!(hits_resp.status(), StatusCode::OK);
let hits_payload: Value = serde_json::from_slice(
&to_bytes(hits_resp.into_body(), usize::MAX)
.await
.expect("hits body"),
)
.expect("hits json");
let hits = hits_payload
.get("hits")
.and_then(Value::as_array)
.cloned()
.expect("hits");
let first_hit = hits.first().cloned().expect("first hit");
assert_eq!(
first_hit
.get("kind")
.or_else(|| first_hit.get("metadata").and_then(|row| row.get("kind")))
.and_then(Value::as_str),
Some("regression_signal")
);
let governed_hit = hits
.iter()
.find(|row| {
row.get("memory_id").and_then(Value::as_str)
== promote_payload.get("memory_id").and_then(Value::as_str)
})
.cloned()
.expect("governed regression signal hit");
assert_eq!(
governed_hit.get("source").and_then(Value::as_str),
Some("governed_memory")
);
assert_eq!(governed_hit.get("same_ref").and_then(Value::as_bool), None);
assert!(governed_hit
.get("content")
.and_then(Value::as_str)
.is_some_and(|content| content.contains("Keep this regression signal")));
}
#[tokio::test]
async fn coder_memory_events_include_normalized_artifact_fields() {
let state = test_state().await;
state
.capability_resolver
.refresh_builtin_bindings()
.await
.expect("refresh builtin bindings");
let mut rx = state.event_bus.subscribe();
let app = app_router(state.clone());
let create_req = Request::builder()
.method("POST")
.uri("/coder/runs")
.header("content-type", "application/json")
.body(Body::from(
json!({
"coder_run_id": "coder-run-memory-events",
"workflow_mode": "issue_triage",
"repo_binding": {
"project_id": "proj-engine",
"workspace_id": "ws-tandem",
"workspace_root": "/tmp/tandem-repo",
"repo_slug": "user123/tandem"
},
"github_ref": {
"kind": "issue",
"number": 335
},
"source_client": "desktop_developer_mode"
})
.to_string(),
))
.expect("create request");
let create_resp = app
.clone()
.oneshot(create_req)
.await
.expect("create response");
assert_eq!(create_resp.status(), StatusCode::OK);
let summary_req = Request::builder()
.method("POST")
.uri("/coder/runs/coder-run-memory-events/triage-summary")
.header("content-type", "application/json")
.body(Body::from(
json!({
"summary": "Capability readiness drift already explained this failure",
"confidence": "high"
})
.to_string(),
))
.expect("summary request");
let summary_resp = app
.clone()
.oneshot(summary_req)
.await
.expect("summary response");
assert_eq!(summary_resp.status(), StatusCode::OK);
let summary_payload: Value = serde_json::from_slice(
&to_bytes(summary_resp.into_body(), usize::MAX)
.await
.expect("summary body"),
)
.expect("summary json");
let triage_candidate_id = summary_payload
.get("generated_candidates")
.and_then(Value::as_array)
.and_then(|rows| {
rows.iter().find_map(|row| {
(row.get("kind").and_then(Value::as_str) == Some("triage_memory")).then(|| {
row.get("candidate_id")
.and_then(Value::as_str)
.map(ToString::to_string)
})?
})
})
.expect("triage candidate id");
let candidate_event = next_event_of_type(&mut rx, "coder.memory.candidate_added").await;
assert_eq!(
candidate_event
.properties
.get("kind")
.and_then(Value::as_str),
Some("memory_candidate")
);
assert_eq!(
candidate_event
.properties
.get("artifact_type")
.and_then(Value::as_str),
Some("coder_memory_candidate")
);
assert!(candidate_event
.properties
.get("artifact_id")
.and_then(Value::as_str)
.is_some());
assert!(candidate_event
.properties
.get("artifact_path")
.and_then(Value::as_str)
.is_some());
let promote_req = Request::builder()
.method("POST")
.uri(format!(
"/coder/runs/coder-run-memory-events/memory-candidates/{triage_candidate_id}/promote"
))
.header("content-type", "application/json")
.body(Body::from(
json!({
"to_tier": "project",
"reviewer_id": "reviewer-1",
"approval_id": "approval-1",
"reason": "approved reusable triage memory"
})
.to_string(),
))
.expect("promote request");
let promote_resp = app
.clone()
.oneshot(promote_req)
.await
.expect("promote response");
assert_eq!(promote_resp.status(), StatusCode::OK);
let promoted_event = next_event_of_type(&mut rx, "coder.memory.promoted").await;
assert_eq!(
promoted_event
.properties
.get("kind")
.and_then(Value::as_str),
Some("memory_promotion")
);
assert_eq!(
promoted_event
.properties
.get("artifact_type")
.and_then(Value::as_str),
Some("coder_memory_promotion")
);
assert!(promoted_event
.properties
.get("artifact_id")
.and_then(Value::as_str)
.is_some());
assert!(promoted_event
.properties
.get("artifact_path")
.and_then(Value::as_str)
.is_some());
}