diff --git a/04-backend/harness-core/src/knowledge_registry.rs b/04-backend/harness-core/src/knowledge_registry.rs index e5a184d5..d7029c5a 100644 --- a/04-backend/harness-core/src/knowledge_registry.rs +++ b/04-backend/harness-core/src/knowledge_registry.rs @@ -464,7 +464,20 @@ impl KnowledgeSourceRegistryService { _req: RegistryActionRequest, ) -> Result { let mut source = self.get_source(source_id)?; - source.status = KnowledgeSourceStatus::Indexed; + match source.status { + KnowledgeSourceStatus::Disabled => { + return Err(HarnessError::InvalidInput(format!( + "cannot ingest disabled knowledge source {source_id}" + ))); + } + KnowledgeSourceStatus::CandidateOnly => { + source.production_enabled = false; + "disabled".clone_into(&mut source.default_route); + } + _ => { + source.status = KnowledgeSourceStatus::Indexed; + } + } source.updated_at = Utc::now(); self.sources.write().insert(source_id.to_owned(), source); let now = Utc::now(); @@ -621,6 +634,20 @@ mod tests { .disable_source("standards", RegistryActionRequest::default()) .expect("source should disable"); assert_eq!(disabled.status, KnowledgeSourceStatus::Disabled); + + assert!( + registry + .ingest_source("standards", RegistryActionRequest::default()) + .is_err(), + "disabled source ingest must be rejected" + ); + assert_eq!( + registry + .get_source("standards") + .expect("source still exists") + .status, + KnowledgeSourceStatus::Disabled + ); } #[test] @@ -655,6 +682,20 @@ mod tests { assert!(!source.production_enabled); assert_eq!(source.default_route, "disabled"); + let ingest = registry + .ingest_source( + "vendor-glendale-optrapid3d", + RegistryActionRequest::default(), + ) + .expect("candidate-only source may record a mock ingest job"); + assert_eq!(ingest.status, "completed"); + let source = registry + .get_source("vendor-glendale-optrapid3d") + .expect("candidate source should still exist"); + assert_eq!(source.status, KnowledgeSourceStatus::CandidateOnly); + assert!(!source.production_enabled); + assert_eq!(source.default_route, "disabled"); + let approved = registry .approve_source( "vendor-glendale-optrapid3d", diff --git a/04-backend/harness-core/src/module_generation.rs b/04-backend/harness-core/src/module_generation.rs index ced86502..adc4ab00 100644 --- a/04-backend/harness-core/src/module_generation.rs +++ b/04-backend/harness-core/src/module_generation.rs @@ -916,14 +916,19 @@ impl ModuleGenerationService { self.mutate_job(job_id, |job| { if matches!( job.status, - GenerationJobStatus::Approved | GenerationJobStatus::Archived + GenerationJobStatus::Approved + | GenerationJobStatus::Rejected + | GenerationJobStatus::Archived ) { return Err(HarnessError::InvalidInput(format!( "cannot reject generation job from {:?}", job.status ))); } - if matches!(job.status, GenerationJobStatus::PendingApproval) { + if matches!( + job.status, + GenerationJobStatus::PendingReview | GenerationJobStatus::PendingApproval + ) { reject_lifecycle( &lifecycle, job.lifecycle_transaction_id, @@ -1563,7 +1568,7 @@ mod tests { use uuid::Uuid; use crate::module_audit::{AuditEventKind, AuditEventQuery, ModuleAuditService}; - use crate::module_lifecycle::ModuleLifecycleService; + use crate::module_lifecycle::{ModuleLifecycleService, ModuleTransactionStatus}; use crate::storage_router::{ ArtifactMetadata, ArtifactRef, ArtifactRole, ArtifactStatus, ArtifactStorageBinding, ArtifactVersion, ElementIdNamespace, GeometryFormat, ViewerAdapterHint, @@ -1576,11 +1581,21 @@ mod tests { }; fn service() -> (ModuleGenerationService, Arc) { + let (service, audit, _lifecycle) = service_with_lifecycle(); + (service, audit) + } + + fn service_with_lifecycle() -> ( + ModuleGenerationService, + Arc, + ModuleLifecycleService, + ) { let audit = Arc::new(ModuleAuditService::new()); let lifecycle = ModuleLifecycleService::new(Arc::clone(&audit)); ( - ModuleGenerationService::new(Arc::clone(&audit), lifecycle), + ModuleGenerationService::new(Arc::clone(&audit), lifecycle.clone()), audit, + lifecycle, ) } @@ -1651,7 +1666,7 @@ mod tests { #[test] fn job_runs_through_review_and_approval() { - let (service, audit) = service(); + let (service, audit, lifecycle) = service_with_lifecycle(); let job = service .create_job(input(GenerationMode::CadToBim)) .expect("job should be created"); @@ -1722,9 +1737,78 @@ mod tests { assert_eq!(job.status, GenerationJobStatus::Approved); assert_eq!(job.artifacts[0].status, ArtifactStatus::Approved); assert_eq!(job.artifacts[0].reference.status, ArtifactStatus::Approved); + let transaction = lifecycle + .get_transaction( + job.lifecycle_transaction_id + .expect("transaction should exist"), + ) + .expect("transaction should still exist"); + assert_eq!(transaction.status, ModuleTransactionStatus::Approved); assert_generation_audit(&audit, &job); } + #[test] + fn pending_review_reject_rejects_linked_lifecycle_transaction() { + let (service, audit, lifecycle) = service_with_lifecycle(); + let job = service + .create_job(input(GenerationMode::ModelToLightweightScene)) + .expect("job should be created"); + let job = service + .plan_job( + job.id, + GenerationActionRequest { + actor: None, + comment: None, + }, + ) + .expect("job should be planned"); + let job = service + .run_job( + job.id, + GenerationActionRequest { + actor: None, + comment: None, + }, + ) + .expect("job should run"); + assert_eq!(job.status, GenerationJobStatus::PendingReview); + + let rejected = service + .reject_job( + job.id, + GenerationActionRequest { + actor: Some("reviewer".to_owned()), + comment: Some("reject before active review".to_owned()), + }, + ) + .expect("pending review job should reject"); + assert_eq!(rejected.status, GenerationJobStatus::Rejected); + let transaction = lifecycle + .get_transaction( + rejected + .lifecycle_transaction_id + .expect("transaction should exist"), + ) + .expect("transaction should still exist"); + assert_eq!(transaction.status, ModuleTransactionStatus::Rejected); + let events = audit + .list(&AuditEventQuery { + module_id: Some("production_manufacturing".to_owned()), + target_type: Some("generation_job".to_owned()), + target_id: Some(rejected.id.to_string()), + actor: None, + limit: Some(20), + cursor: None, + }) + .expect("audit list should work"); + assert!( + events + .items + .iter() + .any(|event| event.action == AuditEventKind::GenerationJobRejected) + ); + } + #[test] fn run_requires_plan_and_review_requires_run() { let (service, _audit) = service();