chore: merge develop into hare/develop
This commit is contained in:
@@ -22,7 +22,7 @@ use crate::records::{
|
||||
TicketEvidenceEvent, TicketEvidenceSummary, TicketListPageRequest, TicketMergeRequestSummary,
|
||||
TicketQueryItem, TicketQueryRequest, TicketQueryResponse, TicketRelationView,
|
||||
TicketRoleAssignmentSummary, TicketShowRequest, TicketSummary, TicketSummaryPage,
|
||||
summarize_body, truncate_body, validate_project_id,
|
||||
summarize_body, truncate_body,
|
||||
};
|
||||
use crate::store::{
|
||||
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
|
||||
@@ -633,7 +633,15 @@ impl SqliteWorkspaceAuthority {
|
||||
predicates.push(format!("o.updated_at<{value}"));
|
||||
}
|
||||
if let Some(value) = &query.linked_ticket_id {
|
||||
let value = bind(SqlValue::Text(value.clone()));
|
||||
let resolved = self
|
||||
.store
|
||||
.resolve_resource_reference(
|
||||
&self.workspace_id,
|
||||
WorkspaceResourceKind::Ticket,
|
||||
value,
|
||||
)?
|
||||
.ok_or_else(|| invalid_objective_error("linked Ticket was not found"))?;
|
||||
let value = bind(SqlValue::Text(resolved));
|
||||
predicates.push(format!("EXISTS (SELECT 1 FROM objective_ticket_links link WHERE link.workspace_id=o.workspace_id AND link.objective_id=o.objective_id AND link.ticket_id={value})"));
|
||||
}
|
||||
let relevance_rank = if let Some(text) =
|
||||
@@ -1237,6 +1245,10 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
||||
.into_iter()
|
||||
.map(|link| link.ticket_id)
|
||||
.collect::<Vec<_>>();
|
||||
let linked_ticket_keys = linked_tickets
|
||||
.iter()
|
||||
.map(|ticket_id| self.resource_key(WorkspaceResourceKind::Ticket, ticket_id))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let body_md = record.body_md.clone();
|
||||
let objective = ObjectiveSummary {
|
||||
resource_key: self
|
||||
@@ -1253,6 +1265,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
||||
items.push(objective_query_item(
|
||||
objective,
|
||||
linked_tickets,
|
||||
linked_ticket_keys,
|
||||
query.query.as_deref(),
|
||||
&body_md,
|
||||
));
|
||||
@@ -1339,9 +1352,19 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
||||
fn create_objective(&self, input: ObjectiveCreateInput) -> Result<ObjectiveDetail> {
|
||||
validate_objective_title(&input.title)?;
|
||||
validate_objective_state(&input.state)?;
|
||||
for ticket_id in &input.linked_tickets {
|
||||
validate_project_id(ticket_id)?;
|
||||
}
|
||||
let linked_tickets = input
|
||||
.linked_tickets
|
||||
.iter()
|
||||
.map(|ticket_reference| {
|
||||
self.store
|
||||
.resolve_resource_reference(
|
||||
&self.workspace_id,
|
||||
WorkspaceResourceKind::Ticket,
|
||||
ticket_reference,
|
||||
)?
|
||||
.ok_or_else(|| invalid_objective_error("linked Ticket was not found"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let now = now_rfc3339();
|
||||
let objective_id = allocate_record_id(
|
||||
unix_epoch_millis_now().map_err(|err| {
|
||||
@@ -1367,8 +1390,7 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
||||
updated_at: now.clone(),
|
||||
};
|
||||
self.store.upsert_objective(&record)?;
|
||||
let links = input
|
||||
.linked_tickets
|
||||
let links = linked_tickets
|
||||
.into_iter()
|
||||
.map(|ticket_id| ObjectiveTicketLinkRecord {
|
||||
workspace_id: self.workspace_id.clone(),
|
||||
@@ -2283,12 +2305,18 @@ fn ticket_query_item(
|
||||
.iter()
|
||||
.map(|objective| objective.id.clone())
|
||||
.collect(),
|
||||
linked_objective_keys: detail
|
||||
.linked_objectives
|
||||
.iter()
|
||||
.map(|objective| objective.resource_key.clone())
|
||||
.collect(),
|
||||
relation_count: detail.relations.outgoing.len() + detail.relations.incoming.len(),
|
||||
blocker_count: detail.relations.blockers.len(),
|
||||
unresolved_blocker_count: detail.relations.blockers.len(),
|
||||
unresolved_review_count: usize::from(detail.evidence.unresolved_request_changes),
|
||||
evidence: detail.evidence.clone(),
|
||||
merge_request: detail.merge_request.clone(),
|
||||
current_coder: detail.current_coder.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2399,6 +2427,7 @@ fn ticket_item_after_cursor(
|
||||
fn objective_query_item(
|
||||
objective: ObjectiveSummary,
|
||||
linked_tickets: Vec<String>,
|
||||
linked_ticket_keys: Vec<String>,
|
||||
text: Option<&str>,
|
||||
body_md: &str,
|
||||
) -> ObjectiveQueryItem {
|
||||
@@ -2426,6 +2455,7 @@ fn objective_query_item(
|
||||
snippet,
|
||||
linked_ticket_count: linked_tickets.len(),
|
||||
linked_tickets,
|
||||
linked_ticket_keys,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3308,16 +3338,21 @@ VALUES ('workspace-test', 'ticket', 4);
|
||||
assert!(!objective.revision.is_empty());
|
||||
assert_eq!(objective.linked_ticket_summaries[0].id, "00000000001J2");
|
||||
assert_eq!(objective.linked_ticket_summaries[0].state, "ready");
|
||||
let linked_ticket_key = objective.linked_ticket_summaries[0].resource_key.clone();
|
||||
let objective_query = authority
|
||||
.query_objectives(ObjectiveQueryRequest {
|
||||
query: Some("Control plane".to_string()),
|
||||
linked_ticket_id: Some("00000000001J2".to_string()),
|
||||
linked_ticket_id: Some(linked_ticket_key.clone()),
|
||||
limit: Some(1),
|
||||
..ObjectiveQueryRequest::default()
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(objective_query.items.len(), 1);
|
||||
assert_eq!(objective_query.items[0].linked_ticket_count, 1);
|
||||
assert_eq!(
|
||||
objective_query.items[0].linked_ticket_keys,
|
||||
vec![linked_ticket_key]
|
||||
);
|
||||
assert_eq!(objective_query.page.limit, 1);
|
||||
let body_query = authority
|
||||
.query_objectives(ObjectiveQueryRequest {
|
||||
@@ -3410,7 +3445,7 @@ VALUES ('workspace-test', 'ticket', 3);
|
||||
title: "Create Objective".to_string(),
|
||||
body_md: "Alpha body".to_string(),
|
||||
state: "active".to_string(),
|
||||
linked_tickets: vec!["00000000001J2".to_string()],
|
||||
linked_tickets: vec!["T-1".to_string()],
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(created.title, "Create Objective");
|
||||
@@ -3436,14 +3471,14 @@ VALUES ('workspace-test', 'ticket', 3);
|
||||
assert_eq!(state.state, "paused");
|
||||
assert_eq!(
|
||||
authority
|
||||
.link_objective_ticket(&created.id, "00000000001J3")
|
||||
.link_objective_ticket(&created.id, "T-2")
|
||||
.unwrap()
|
||||
.linked_tickets,
|
||||
vec!["00000000001J2", "00000000001J3"]
|
||||
);
|
||||
assert_eq!(
|
||||
authority
|
||||
.unlink_objective_ticket(&created.id, "00000000001J2")
|
||||
.unlink_objective_ticket(&created.id, "T-1")
|
||||
.unwrap()
|
||||
.linked_tickets,
|
||||
vec!["00000000001J3"]
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use project_record::validate_record_id;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use workspace_api::{
|
||||
ObjectiveDetail, ObjectiveEventDetail, ObjectiveLinkedTicketSummary, ObjectiveResourceSummary,
|
||||
ObjectiveSummary, QueryPage,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
const SUMMARY_BODY_LIMIT: usize = 240;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -387,12 +384,16 @@ pub struct TicketQueryItem {
|
||||
pub snippet: Option<String>,
|
||||
pub matching_event: Option<TicketEvidenceEvent>,
|
||||
pub linked_objective_ids: Vec<String>,
|
||||
#[ts(skip)]
|
||||
pub linked_objective_keys: Vec<String>,
|
||||
pub relation_count: usize,
|
||||
pub blocker_count: usize,
|
||||
pub unresolved_blocker_count: usize,
|
||||
pub unresolved_review_count: usize,
|
||||
pub evidence: TicketEvidenceSummary,
|
||||
pub merge_request: Option<TicketMergeRequestSummary>,
|
||||
#[ts(skip)]
|
||||
pub current_coder: Option<TicketAssignmentSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -435,6 +436,7 @@ pub struct ObjectiveQueryItem {
|
||||
pub snippet: Option<String>,
|
||||
pub linked_ticket_count: usize,
|
||||
pub linked_tickets: Vec<String>,
|
||||
pub linked_ticket_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -521,10 +523,6 @@ mod typescript_tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_project_id(id: &str) -> Result<()> {
|
||||
validate_record_id(id).map_err(|_| Error::InvalidRecordId(id.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn summarize_body(body: &str) -> String {
|
||||
let summary = body
|
||||
.lines()
|
||||
|
||||
@@ -327,8 +327,6 @@ fn repository_local_path(source: &workspace_api::RepositorySource) -> Option<Pat
|
||||
}
|
||||
}
|
||||
|
||||
const ORCHESTRATOR_ATTENTION_TICKET_LIMIT: usize = 20;
|
||||
const ORCHESTRATOR_ATTENTION_PROMPT_NAME: &str = "internal.workspace_orchestrator_queue_attention";
|
||||
static EMBEDDED_RUNTIME_REQUEST_IDENTITY: std::sync::LazyLock<
|
||||
worker_runtime::auth::RuntimeIdentityMaterial,
|
||||
> = std::sync::LazyLock::new(|| {
|
||||
@@ -7257,25 +7255,21 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shown = queued
|
||||
.iter()
|
||||
.take(ORCHESTRATOR_ATTENTION_TICKET_LIMIT)
|
||||
.map(|ticket| {
|
||||
format!(
|
||||
"- {} — {}",
|
||||
bounded_orchestrator_attention_text(&ticket.id, 80),
|
||||
bounded_orchestrator_attention_text(&ticket.title, 240)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let omitted = queued
|
||||
.len()
|
||||
.saturating_sub(ORCHESTRATOR_ATTENTION_TICKET_LIMIT);
|
||||
let omitted_line = if omitted == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("Additional queued Tickets omitted from this notice: {omitted}\n")
|
||||
let attention_context = match orchestrator_queue_attention_context(
|
||||
&api.config.workspace_id,
|
||||
&api.config.workspace_id,
|
||||
&queued,
|
||||
) {
|
||||
Ok(context) => context,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
workspace_id = %api.config.workspace_id,
|
||||
candidate_count = queued.len(),
|
||||
diagnostic = error,
|
||||
"orchestrator backlog attention projection rejected"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Ok(Some(config_state)) = api
|
||||
.config_store
|
||||
@@ -7292,16 +7286,19 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
|
||||
let Ok(catalog) = worker::PromptCatalog::from_projection(projection.catalog().clone()) else {
|
||||
return;
|
||||
};
|
||||
let content = match catalog.render_serializable(
|
||||
ORCHESTRATOR_ATTENTION_PROMPT_NAME,
|
||||
&BTreeMap::from([
|
||||
("omitted_line", omitted_line.as_str()),
|
||||
("workspace_id", api.config.workspace_id.as_str()),
|
||||
("ticket_lines", shown.as_str()),
|
||||
]),
|
||||
let content = match catalog.orchestrator_queue_attention(
|
||||
worker::OrchestratorQueueAttentionPrompt::Server,
|
||||
&attention_context,
|
||||
) {
|
||||
Ok(content) => content,
|
||||
Err(_) => return,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
workspace_id = %api.config.workspace_id,
|
||||
diagnostic = %error,
|
||||
"orchestrator backlog attention rendering failed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let accepted = api
|
||||
.runtime
|
||||
@@ -7321,20 +7318,26 @@ fn dispatch_orchestrator_queue_attention(api: &WorkspaceApi) {
|
||||
}
|
||||
}
|
||||
|
||||
fn bounded_orchestrator_attention_text(input: &str, max_chars: usize) -> String {
|
||||
let mut output = String::new();
|
||||
for (index, character) in input.chars().enumerate() {
|
||||
if index == max_chars {
|
||||
output.push('…');
|
||||
break;
|
||||
}
|
||||
output.push(if character.is_control() {
|
||||
' '
|
||||
} else {
|
||||
character
|
||||
});
|
||||
fn orchestrator_queue_attention_context(
|
||||
expected_workspace_id: &str,
|
||||
candidate_workspace_id: &str,
|
||||
tickets: &[ticket::TicketSummary],
|
||||
) -> std::result::Result<worker::OrchestratorQueueAttentionContext, &'static str> {
|
||||
if candidate_workspace_id != expected_workspace_id {
|
||||
return Err("foreign_workspace_ticket_projection");
|
||||
}
|
||||
output
|
||||
let tickets = tickets
|
||||
.iter()
|
||||
.map(|ticket| {
|
||||
let resource_key = ticket
|
||||
.resource_key
|
||||
.clone()
|
||||
.ok_or("missing_ticket_resource_key")?;
|
||||
worker::OrchestratorQueueAttentionTicket::new(resource_key, ticket.title.clone())
|
||||
.map_err(|_| "invalid_ticket_resource_key")
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
Ok(worker::OrchestratorQueueAttentionContext::new(tickets))
|
||||
}
|
||||
|
||||
fn require_online_workspace_orchestrator_source(
|
||||
@@ -9258,9 +9261,8 @@ fn cleanup_working_directory_for_runtime(
|
||||
result.diagnostics,
|
||||
));
|
||||
};
|
||||
let record = workdir_record_from_summary(&api, runtime_id, &working_directory.summary);
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
let mut summary = working_directory.summary;
|
||||
persist_workdir_cleanup_observation(&api, runtime_id, &summary)?;
|
||||
apply_workdir_occupancy_projection(&api, &mut summary)?;
|
||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
@@ -14151,11 +14153,7 @@ fn sync_runtime_workdir_observations(
|
||||
api.store.upsert_workdir_registry(&updated)?;
|
||||
}
|
||||
} else {
|
||||
record.materialization_status =
|
||||
workdir_status_from_runtime_miss(result.diagnostics.as_slice()).to_string();
|
||||
record.cleanliness = "unknown".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
persist_workdir_runtime_miss(api, record, result.diagnostics.as_slice())?;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -14169,17 +14167,54 @@ fn sync_runtime_workdir_observations(
|
||||
Ok(response.diagnostics)
|
||||
}
|
||||
|
||||
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
|
||||
if diagnostics
|
||||
fn persist_workdir_cleanup_observation(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
summary: &WorkingDirectorySummary,
|
||||
) -> ApiResult<()> {
|
||||
if summary.status == WorkingDirectoryStatusKind::NotFound {
|
||||
api.store.delete_workdir_registry(
|
||||
&api.config.workspace_id,
|
||||
summary.working_directory_id.as_str(),
|
||||
)?;
|
||||
} else {
|
||||
let record = workdir_record_from_summary(api, runtime_id, summary);
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn workdir_runtime_miss_is_not_found(diagnostics: &[RuntimeDiagnostic]) -> bool {
|
||||
diagnostics
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.code == "working_directory_not_found")
|
||||
{
|
||||
}
|
||||
|
||||
fn workdir_status_from_runtime_miss(diagnostics: &[RuntimeDiagnostic]) -> &'static str {
|
||||
if workdir_runtime_miss_is_not_found(diagnostics) {
|
||||
"not_found"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_workdir_runtime_miss(
|
||||
api: &WorkspaceApi,
|
||||
mut record: WorkdirRegistryRecord,
|
||||
diagnostics: &[RuntimeDiagnostic],
|
||||
) -> ApiResult<()> {
|
||||
if workdir_runtime_miss_is_not_found(diagnostics) {
|
||||
api.store
|
||||
.delete_workdir_registry(&api.config.workspace_id, record.workdir_id.as_str())?;
|
||||
} else {
|
||||
record.materialization_status = "unknown".to_string();
|
||||
record.cleanliness = "unknown".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_all_runtime_workdir_observations(api: &WorkspaceApi) -> Vec<RuntimeDiagnostic> {
|
||||
let mut diagnostics = Vec::new();
|
||||
let runtimes = api.runtime.list_runtimes(api.config.max_records.min(200));
|
||||
@@ -16977,22 +17012,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn workdir_runtime_miss_uses_exact_typed_code() {
|
||||
let typed_not_found = [RuntimeDiagnostic {
|
||||
code: "working_directory_not_found".to_string(),
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
message: "missing".to_string(),
|
||||
}];
|
||||
assert_eq!(
|
||||
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
|
||||
code: "working_directory_not_found".to_string(),
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
message: "missing".to_string(),
|
||||
}]),
|
||||
workdir_status_from_runtime_miss(&typed_not_found),
|
||||
"not_found"
|
||||
);
|
||||
assert_eq!(
|
||||
workdir_status_from_runtime_miss(&[RuntimeDiagnostic {
|
||||
code: "some_other_not_found".to_string(),
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
message: "not a typed workdir miss".to_string(),
|
||||
}]),
|
||||
"unknown"
|
||||
);
|
||||
assert!(workdir_runtime_miss_is_not_found(&typed_not_found));
|
||||
|
||||
let unrelated = [RuntimeDiagnostic {
|
||||
code: "some_other_not_found".to_string(),
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
message: "not a typed workdir miss".to_string(),
|
||||
}];
|
||||
assert_eq!(workdir_status_from_runtime_miss(&unrelated), "unknown");
|
||||
assert!(!workdir_runtime_miss_is_not_found(&unrelated));
|
||||
}
|
||||
|
||||
struct DeterministicExecutionBackend {
|
||||
@@ -19615,7 +19652,8 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn orchestrator_running_to_idle_recovers_queued_ticket_without_notification_memory() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let api = test_api(dir.path()).await;
|
||||
init_clean_git_workspace(dir.path());
|
||||
let (api, execution) = test_api_with_recording_backend(dir.path()).await;
|
||||
let backend = browser_ticket_backend(&api).unwrap();
|
||||
let mut input = ticket::NewTicket::new("Recover queued work");
|
||||
input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
@@ -19634,6 +19672,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(started.online);
|
||||
let startup_inputs = execution.take_inputs();
|
||||
assert_eq!(startup_inputs.len(), 1);
|
||||
assert_eq!(
|
||||
api.orchestrator_attention_fingerprint
|
||||
.lock()
|
||||
@@ -19669,6 +19709,76 @@ mod tests {
|
||||
.as_deref(),
|
||||
Some(ticket_ref.id.as_str())
|
||||
);
|
||||
let notifications = execution.take_inputs();
|
||||
assert_eq!(notifications.len(), 1);
|
||||
assert_eq!(notifications[0].0.worker_id.to_string(), worker_id);
|
||||
let content = ¬ifications[0].1;
|
||||
assert!(content.starts_with("Queued Tickets require attention:"));
|
||||
assert!(
|
||||
content.contains(&format!(
|
||||
"- {} — Recover queued work",
|
||||
ticket_ref.resource_key.as_deref().unwrap()
|
||||
)),
|
||||
"unexpected notification body: {content:?}"
|
||||
);
|
||||
assert!(content.contains("Reread the current Ticket state before acting"));
|
||||
assert!(!content.contains(ticket_ref.id.as_str()));
|
||||
assert!(!content.contains(TEST_WORKSPACE_ID));
|
||||
assert!(!content.contains("bounded"));
|
||||
assert!(!content.contains("omitted"));
|
||||
|
||||
let candidates = backend
|
||||
.list(ticket::TicketListQuery::states([
|
||||
ticket::TicketListState::Queued,
|
||||
]))
|
||||
.unwrap();
|
||||
let mut truncated_candidates = (1..=worker::OrchestratorQueueAttentionContext::MAX_TICKETS
|
||||
+ 1)
|
||||
.map(|index| {
|
||||
let mut candidate = candidates[0].clone();
|
||||
candidate.id = format!("opaque-{index}");
|
||||
candidate.resource_key = Some(format!("T-{index}"));
|
||||
candidate.title = format!("Queued {index}");
|
||||
candidate
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let truncated = orchestrator_queue_attention_context(
|
||||
TEST_WORKSPACE_ID,
|
||||
TEST_WORKSPACE_ID,
|
||||
&truncated_candidates,
|
||||
)
|
||||
.unwrap();
|
||||
let rendered = worker::PromptCatalog::builtins_only()
|
||||
.unwrap()
|
||||
.orchestrator_queue_attention(
|
||||
worker::OrchestratorQueueAttentionPrompt::Server,
|
||||
&truncated,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rendered.contains("- T-20 — Queued 20"));
|
||||
assert!(!rendered.contains("T-21"));
|
||||
assert!(rendered.contains("were omitted from this notice: 1"));
|
||||
assert!(!rendered.contains("opaque-"));
|
||||
|
||||
truncated_candidates[0].resource_key = None;
|
||||
assert_eq!(
|
||||
orchestrator_queue_attention_context(
|
||||
TEST_WORKSPACE_ID,
|
||||
TEST_WORKSPACE_ID,
|
||||
&truncated_candidates
|
||||
)
|
||||
.unwrap_err(),
|
||||
"missing_ticket_resource_key"
|
||||
);
|
||||
assert_eq!(
|
||||
orchestrator_queue_attention_context(
|
||||
TEST_WORKSPACE_ID,
|
||||
"foreign-workspace",
|
||||
&truncated_candidates
|
||||
)
|
||||
.unwrap_err(),
|
||||
"foreign_workspace_ticket_projection"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -21232,6 +21342,87 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn confirmed_runtime_miss_removes_registry_record_but_unknown_is_retained() {
|
||||
let workspace = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(workspace.path());
|
||||
let api = test_api(workspace.path()).await;
|
||||
seed_cleanup_workdir(&api, "deleted-workdir", "present", "clean");
|
||||
let deleted = api
|
||||
.store
|
||||
.get_workdir_registry(TEST_WORKSPACE_ID, "deleted-workdir")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
persist_workdir_runtime_miss(
|
||||
&api,
|
||||
deleted,
|
||||
&[RuntimeDiagnostic {
|
||||
code: "working_directory_not_found".to_string(),
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
message: "missing".to_string(),
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
api.store
|
||||
.get_workdir_registry(TEST_WORKSPACE_ID, "deleted-workdir")
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
|
||||
seed_cleanup_workdir(&api, "unknown-workdir", "present", "clean");
|
||||
let unknown = api
|
||||
.store
|
||||
.get_workdir_registry(TEST_WORKSPACE_ID, "unknown-workdir")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
persist_workdir_runtime_miss(
|
||||
&api,
|
||||
unknown,
|
||||
&[RuntimeDiagnostic {
|
||||
code: "runtime_unavailable".to_string(),
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
message: "temporarily unavailable".to_string(),
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
api.store
|
||||
.get_workdir_registry(TEST_WORKSPACE_ID, "unknown-workdir")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.materialization_status,
|
||||
"unknown"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleanup_not_found_observation_removes_registry_record() {
|
||||
let workspace = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(workspace.path());
|
||||
let api = test_api(workspace.path()).await;
|
||||
let working_directory_id = "cleanup-existing";
|
||||
seed_cleanup_workdir(&api, working_directory_id, "present", "clean");
|
||||
let record = api
|
||||
.store
|
||||
.get_workdir_registry(TEST_WORKSPACE_ID, working_directory_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let mut summary = workdir_summary_from_record(&record);
|
||||
summary.status = WorkingDirectoryStatusKind::NotFound;
|
||||
|
||||
persist_workdir_cleanup_observation(&api, "runtime-test", &summary).unwrap();
|
||||
|
||||
assert!(
|
||||
api.store
|
||||
.get_workdir_registry(TEST_WORKSPACE_ID, working_directory_id)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
fn seed_cleanup_link(api: &WorkspaceApi, runtime_worker_id: &str, workdir_id: &str) {
|
||||
let runtime_worker_id = runtime_worker_id.parse::<u64>().unwrap();
|
||||
api.store
|
||||
|
||||
Reference in New Issue
Block a user