diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 74f890c14..2c8a90dc5 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -3707,14 +3707,6 @@ fn validated_ticket_implementation_targets( repository.repository_key ))) })?; - api.live_repository_reader()? - .observe_merge_target(&repository_id, Some(&ref_selector)) - .map_err(|error| { - ApiError::from(Error::InvalidInput(format!( - "Ticket `{ticket_id}` target Repository `{}` selector `{ref_selector}` is not resolvable: {error:?}", - repository.repository_key - ))) - })?; let capabilities = match target.access { TicketTargetAccess::ReadOnly => { workdir::WorkdirSessionCapabilities::READ_ONLY @@ -10932,15 +10924,6 @@ impl ticket::TicketTargetAuthority for WorkspaceTicketTargetAuthority { .ok_or_else(|| { ticket::TicketError::MissingTargetSelector(repository.repository_key.clone()) })?; - self.api - .live_repository_reader() - .map_err(|error| ticket::TicketError::Conflict(error.to_string()))? - .observe_merge_target(&repository.repository_id, Some(selector)) - .map_err(|error| ticket::TicketError::InvalidTargetSelector { - repository_key: repository.repository_key.clone(), - selector: selector.to_owned(), - reason: format!("{error:?}"), - })?; Ok(ticket::ResolvedTicketTarget { repository_key: repository.repository_key, ref_selector: selector.to_owned(), @@ -33123,10 +33106,9 @@ mod tests { } #[tokio::test] - async fn mark_ready_resolves_workspace_target_and_closes_lifecycle_bypasses() { + async fn mark_ready_accepts_registered_remote_target_and_closes_lifecycle_bypasses() { let dir = tempfile::tempdir().unwrap(); - init_clean_git_workspace(dir.path()); - let api = test_api(dir.path()).await; + let api = test_api_with_remote_repository(dir.path()).await; let backend = browser_ticket_backend(&api).unwrap(); let repository_key = "test-repository"; @@ -33151,6 +33133,15 @@ mod tests { TicketTargetAccess::ReadWrite, )] ); + let implementation_targets = + validated_ticket_implementation_targets(&api, &ticket_ref.id).unwrap(); + assert_eq!(implementation_targets.len(), 1); + assert_eq!(implementation_targets[0].repository_key, repository_key); + assert_eq!(implementation_targets[0].ref_selector, "develop"); + assert_eq!( + implementation_targets[0].access, + TicketTargetAccess::ReadWrite + ); assert_eq!( backend .mark_ready(TicketIdOrSlug::Id(ticket_ref.id.clone()), request) @@ -34596,14 +34587,13 @@ mod tests { } #[tokio::test] - async fn browser_queue_eligibility_uses_authoritative_dependency_targets() { + async fn browser_queue_eligibility_accepts_registered_remote_dependency_targets() { let dir = tempfile::tempdir().unwrap(); - init_clean_git_workspace(dir.path()); - let api = test_api(dir.path()).await; + let api = test_api_with_remote_repository(dir.path()).await; let backend = browser_ticket_backend(&api).unwrap(); - let mut dependency_input = ticket::NewTicket::new("Invalid target dependency"); + let mut dependency_input = ticket::NewTicket::new("Remote target dependency"); dependency_input.workflow_state = Some(TicketWorkflowState::Ready); - set_test_ticket_target(&mut dependency_input, "test-repository", "missing-ref"); + set_test_ticket_target(&mut dependency_input, "test-repository", "remote-feature"); let dependency = backend.create(dependency_input).unwrap(); let mut root_input = ticket::NewTicket::new("Queue root"); root_input.workflow_state = Some(TicketWorkflowState::Ready); @@ -34624,25 +34614,23 @@ mod tests { assign_test_orchestrator(&api, &dependency.id); let Json(detail) = browser_ticket_detail(&api, &root.id).unwrap(); - assert!(!detail.action_eligibility.can_queue); - assert!( - detail - .action_eligibility - .blockers - .iter() - .any(|blocker| blocker.contains("missing-ref")) - ); - let result = scoped_queue_ticket_record( + assert!(detail.action_eligibility.can_queue); + assert!(detail.action_eligibility.blockers.is_empty()); + let Json(outcome) = scoped_queue_ticket_record( State(api.clone()), AxumPath((TEST_WORKSPACE_ID.to_string(), root.id.clone())), HeaderMap::new(), ) - .await; - assert!(result.is_err()); + .await + .unwrap(); + assert_eq!( + outcome.queued_tickets, + vec![dependency.id.clone(), root.id.clone()] + ); for ticket_id in [dependency.id, root.id] { assert_eq!( backend.show(ticket_id.into()).unwrap().meta.workflow_state, - TicketWorkflowState::Ready + TicketWorkflowState::Queued ); } } @@ -35712,6 +35700,27 @@ mod tests { test_api_with_recording_backend(workspace_root).await.0 } + async fn test_api_with_remote_repository(workspace_root: impl Into) -> WorkspaceApi { + let mut config = test_server_config(workspace_root); + let source = server_api::RepositorySource { + kind: server_api::RepositorySourceKind::Ssh, + uri: "git@example.invalid:owner/repository.git".to_string(), + }; + config.repositories[0].source_fingerprint = + crate::repository_source::repository_source_fingerprint(&source); + config.repositories[0].source = source; + config.repositories[0].path = None; + config.repositories[0].default_selector = Some("develop".to_string()); + let store = SqliteWorkspaceStore::open(config.database_path.clone()).unwrap(); + WorkspaceApi::new_with_execution_backend( + config, + Arc::new(store), + Arc::new(DeterministicExecutionBackend::default()), + ) + .await + .unwrap() + } + async fn test_api_with_docs_repository(workspace_root: impl Into) -> WorkspaceApi { let workspace_root = workspace_root.into(); let docs_root = workspace_root.join("docs-repository"); diff --git a/web/workspace/src/lib/workspace/tickets/ticket-panel.test.ts b/web/workspace/src/lib/workspace/tickets/ticket-panel.test.ts index 5908f7d51..8bd8ef4fe 100644 --- a/web/workspace/src/lib/workspace/tickets/ticket-panel.test.ts +++ b/web/workspace/src/lib/workspace/tickets/ticket-panel.test.ts @@ -184,6 +184,14 @@ Deno.test("ticket detail uses server-derived role assignment actions", async () assertEquals(source.includes("updatedTicket.targets"), true); assertEquals(source.includes("ticket.repository_key"), false); assertEquals(source.includes("ticket.ref_selector"), false); + assertEquals( + source.includes("(repository.diagnostics ?? []).length > 0"), + false, + ); + assertEquals( + source.includes("unique registered repository targets"), + true, + ); }); Deno.test("Ticket authoring creates the target collection without singular authority", async () => { diff --git a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte index b8657ca6c..d5215ba3f 100644 --- a/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/tickets/[ticketId]/+page.svelte @@ -114,7 +114,6 @@ const repository = repositoryFor(target.repository_key); if ( !target.repository_key || repository === null || - (repository.diagnostics ?? []).length > 0 || !effectiveRefSelector(target) || repositoryKeys.has(target.repository_key) ) return false; @@ -651,7 +650,7 @@ {busy === "ready" ? "Marking ready…" : "Mark ready"} {#if !targetCandidateValid} -

Add unique healthy repository targets with effective ref selectors and exactly one read-write target before marking ready.

+

Add unique registered repository targets with effective ref selectors and exactly one read-write target before marking ready.

{/if} {:else if ticket.state === "ready"}