fix: allow remote ticket targets without server checkout

This commit is contained in:
2026-09-24 05:04:19 +09:00
parent 70eb1120a2
commit e6608dd42f
3 changed files with 55 additions and 39 deletions
+46 -37
View File
@@ -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<PathBuf>) -> 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<PathBuf>) -> WorkspaceApi {
let workspace_root = workspace_root.into();
let docs_root = workspace_root.join("docs-repository");
@@ -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 () => {
@@ -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"}
</button>
{#if !targetCandidateValid}
<p class="workspace-empty-copy">Add unique healthy repository targets with effective ref selectors and exactly one read-write target before marking ready.</p>
<p class="workspace-empty-copy">Add unique registered repository targets with effective ref selectors and exactly one read-write target before marking ready.</p>
{/if}
{:else if ticket.state === "ready"}
<button class="workspace-primary-button ticket-queue-button" type="button" disabled={busy === "queue" || !ticket.action_eligibility.can_queue} onclick={() => void queueTicket()}>