fix: allow remote ticket targets without server checkout
This commit is contained in:
@@ -3707,14 +3707,6 @@ fn validated_ticket_implementation_targets(
|
|||||||
repository.repository_key
|
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 {
|
let capabilities = match target.access {
|
||||||
TicketTargetAccess::ReadOnly => {
|
TicketTargetAccess::ReadOnly => {
|
||||||
workdir::WorkdirSessionCapabilities::READ_ONLY
|
workdir::WorkdirSessionCapabilities::READ_ONLY
|
||||||
@@ -10932,15 +10924,6 @@ impl ticket::TicketTargetAuthority for WorkspaceTicketTargetAuthority {
|
|||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ticket::TicketError::MissingTargetSelector(repository.repository_key.clone())
|
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 {
|
Ok(ticket::ResolvedTicketTarget {
|
||||||
repository_key: repository.repository_key,
|
repository_key: repository.repository_key,
|
||||||
ref_selector: selector.to_owned(),
|
ref_selector: selector.to_owned(),
|
||||||
@@ -33123,10 +33106,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
init_clean_git_workspace(dir.path());
|
let api = test_api_with_remote_repository(dir.path()).await;
|
||||||
let api = test_api(dir.path()).await;
|
|
||||||
let backend = browser_ticket_backend(&api).unwrap();
|
let backend = browser_ticket_backend(&api).unwrap();
|
||||||
|
|
||||||
let repository_key = "test-repository";
|
let repository_key = "test-repository";
|
||||||
@@ -33151,6 +33133,15 @@ mod tests {
|
|||||||
TicketTargetAccess::ReadWrite,
|
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!(
|
assert_eq!(
|
||||||
backend
|
backend
|
||||||
.mark_ready(TicketIdOrSlug::Id(ticket_ref.id.clone()), request)
|
.mark_ready(TicketIdOrSlug::Id(ticket_ref.id.clone()), request)
|
||||||
@@ -34596,14 +34587,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
init_clean_git_workspace(dir.path());
|
let api = test_api_with_remote_repository(dir.path()).await;
|
||||||
let api = test_api(dir.path()).await;
|
|
||||||
let backend = browser_ticket_backend(&api).unwrap();
|
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);
|
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 dependency = backend.create(dependency_input).unwrap();
|
||||||
let mut root_input = ticket::NewTicket::new("Queue root");
|
let mut root_input = ticket::NewTicket::new("Queue root");
|
||||||
root_input.workflow_state = Some(TicketWorkflowState::Ready);
|
root_input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||||
@@ -34624,25 +34614,23 @@ mod tests {
|
|||||||
assign_test_orchestrator(&api, &dependency.id);
|
assign_test_orchestrator(&api, &dependency.id);
|
||||||
|
|
||||||
let Json(detail) = browser_ticket_detail(&api, &root.id).unwrap();
|
let Json(detail) = browser_ticket_detail(&api, &root.id).unwrap();
|
||||||
assert!(!detail.action_eligibility.can_queue);
|
assert!(detail.action_eligibility.can_queue);
|
||||||
assert!(
|
assert!(detail.action_eligibility.blockers.is_empty());
|
||||||
detail
|
let Json(outcome) = scoped_queue_ticket_record(
|
||||||
.action_eligibility
|
|
||||||
.blockers
|
|
||||||
.iter()
|
|
||||||
.any(|blocker| blocker.contains("missing-ref"))
|
|
||||||
);
|
|
||||||
let result = scoped_queue_ticket_record(
|
|
||||||
State(api.clone()),
|
State(api.clone()),
|
||||||
AxumPath((TEST_WORKSPACE_ID.to_string(), root.id.clone())),
|
AxumPath((TEST_WORKSPACE_ID.to_string(), root.id.clone())),
|
||||||
HeaderMap::new(),
|
HeaderMap::new(),
|
||||||
)
|
)
|
||||||
.await;
|
.await
|
||||||
assert!(result.is_err());
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
outcome.queued_tickets,
|
||||||
|
vec![dependency.id.clone(), root.id.clone()]
|
||||||
|
);
|
||||||
for ticket_id in [dependency.id, root.id] {
|
for ticket_id in [dependency.id, root.id] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
backend.show(ticket_id.into()).unwrap().meta.workflow_state,
|
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
|
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 {
|
async fn test_api_with_docs_repository(workspace_root: impl Into<PathBuf>) -> WorkspaceApi {
|
||||||
let workspace_root = workspace_root.into();
|
let workspace_root = workspace_root.into();
|
||||||
let docs_root = workspace_root.join("docs-repository");
|
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("updatedTicket.targets"), true);
|
||||||
assertEquals(source.includes("ticket.repository_key"), false);
|
assertEquals(source.includes("ticket.repository_key"), false);
|
||||||
assertEquals(source.includes("ticket.ref_selector"), 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 () => {
|
Deno.test("Ticket authoring creates the target collection without singular authority", async () => {
|
||||||
|
|||||||
@@ -114,7 +114,6 @@
|
|||||||
const repository = repositoryFor(target.repository_key);
|
const repository = repositoryFor(target.repository_key);
|
||||||
if (
|
if (
|
||||||
!target.repository_key || repository === null ||
|
!target.repository_key || repository === null ||
|
||||||
(repository.diagnostics ?? []).length > 0 ||
|
|
||||||
!effectiveRefSelector(target) ||
|
!effectiveRefSelector(target) ||
|
||||||
repositoryKeys.has(target.repository_key)
|
repositoryKeys.has(target.repository_key)
|
||||||
) return false;
|
) return false;
|
||||||
@@ -651,7 +650,7 @@
|
|||||||
{busy === "ready" ? "Marking ready…" : "Mark ready"}
|
{busy === "ready" ? "Marking ready…" : "Mark ready"}
|
||||||
</button>
|
</button>
|
||||||
{#if !targetCandidateValid}
|
{#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}
|
{/if}
|
||||||
{:else if ticket.state === "ready"}
|
{: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()}>
|
<button class="workspace-primary-button ticket-queue-button" type="button" disabled={busy === "queue" || !ticket.action_eligibility.can_queue} onclick={() => void queueTicket()}>
|
||||||
|
|||||||
Reference in New Issue
Block a user