fix: preserve external Workdir Workers in sidebar

This commit is contained in:
2026-09-29 15:29:10 +09:00
parent 8c78e55b28
commit 39b4a019c9
2 changed files with 96 additions and 10 deletions
+41 -2
View File
@@ -599,7 +599,8 @@ pub struct SubscriptionWorkerWorkdirAttachment {
/// Stable Worker-local routing alias. /// Stable Worker-local routing alias.
pub alias: String, pub alias: String,
/// Runtime-internal Repository id. Workspace-facing TypeScript contracts /// Runtime-internal Repository id. Workspace-facing TypeScript contracts
/// omit this field and require `repository_key` from the Server projection. /// omit this field and use `repository_key` for Repository Workdirs. Both
/// Repository fields are absent for non-Repository Workdirs.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(skip))] #[cfg_attr(feature = "typescript", ts(skip))]
#[cfg_attr(feature = "json-schema", schemars(skip))] #[cfg_attr(feature = "json-schema", schemars(skip))]
@@ -622,7 +623,8 @@ impl SubscriptionWorkerWorkdirAttachment {
validate_identifier("repository_id", repository_id, MAX_RESOURCE_ID_BYTES) validate_identifier("repository_id", repository_id, MAX_RESOURCE_ID_BYTES)
} }
(None, Some(repository_key)) => validate_repository_key(repository_key), (None, Some(repository_key)) => validate_repository_key(repository_key),
_ => Err(SubscriptionValidationError::InvalidIdentifier { (None, None) => Ok(()),
(Some(_), Some(_)) => Err(SubscriptionValidationError::InvalidIdentifier {
field: "repository_authority", field: "repository_authority",
}), }),
} }
@@ -1009,6 +1011,43 @@ mod tests {
assert!(workdir_json.get("repository_id").is_none()); assert!(workdir_json.get("repository_id").is_none());
} }
#[test]
fn external_worker_attachment_has_no_repository_authority() {
let mut external_worker = worker("worker-1");
external_worker
.workdir_attachments
.push(SubscriptionWorkerWorkdirAttachment {
alias: "external".to_string(),
repository_id: None,
repository_key: None,
working_directory_id: SubscriptionWorkdirId::new("external-workdir-1").unwrap(),
});
external_worker.validate().unwrap();
let json = serde_json::to_value(&external_worker).unwrap();
let attachment = &json["workdir_attachments"][0];
assert!(attachment.get("repository_id").is_none());
assert!(attachment.get("repository_key").is_none());
assert_eq!(attachment["working_directory_id"], "external-workdir-1");
}
#[test]
fn worker_attachment_rejects_ambiguous_repository_authority() {
let attachment = SubscriptionWorkerWorkdirAttachment {
alias: "checkout".to_string(),
repository_id: Some("repository-id".to_string()),
repository_key: Some("repository-key".to_string()),
working_directory_id: SubscriptionWorkdirId::new("workdir-1").unwrap(),
};
assert!(matches!(
attachment.validate(),
Err(SubscriptionValidationError::InvalidIdentifier {
field: "repository_authority"
})
));
}
#[test] #[test]
fn subscribe_frame_has_stable_versioned_json_shape() { fn subscribe_frame_has_stable_versioned_json_shape() {
let frame = SubscriptionFrame::new(SubscriptionFramePayload::Request( let frame = SubscriptionFrame::new(SubscriptionFramePayload::Request(
@@ -513,20 +513,29 @@ fn project_working_directory(
} }
fn project_repository_keys(api: &WorkspaceApi, worker: &mut SubscriptionWorker) -> bool { fn project_repository_keys(api: &WorkspaceApi, worker: &mut SubscriptionWorker) -> bool {
project_repository_keys_with(worker, |repository_id| {
api.store
.get_repository(&api.config.workspace_id, repository_id)
.ok()
.flatten()
.map(|repository| repository.repository_key)
})
}
fn project_repository_keys_with(
worker: &mut SubscriptionWorker,
mut resolve_repository_key: impl FnMut(&str) -> Option<String>,
) -> bool {
for attachment in &mut worker.workdir_attachments { for attachment in &mut worker.workdir_attachments {
let Some(repository_id) = attachment.repository_id.take() else { let Some(repository_id) = attachment.repository_id.take() else {
if attachment.repository_key.is_none() { // External Workdirs have no Repository identity. Their stable
return false; // working_directory_id joins against the Workspace Workdir projection.
}
continue; continue;
}; };
let Ok(Some(repository)) = api let Some(repository_key) = resolve_repository_key(&repository_id) else {
.store
.get_repository(&api.config.workspace_id, &repository_id)
else {
return false; return false;
}; };
attachment.repository_key = Some(repository.repository_key); attachment.repository_key = Some(repository_key);
} }
true true
} }
@@ -593,6 +602,44 @@ mod tests {
protocol::subscription::SubscriptionWorkerId::new("worker-1").unwrap() protocol::subscription::SubscriptionWorkerId::new("worker-1").unwrap()
} }
#[test]
fn external_workdir_attachment_does_not_remove_workspace_worker() {
let mut worker = SubscriptionWorker {
worker_id: worker_id(),
runtime_id: Some("runtime-1".to_string()),
resource_key: Some("W-1".to_string()),
availability: protocol::subscription::SubscriptionWorkerAvailability::Observed,
subject_revision: 1,
worker_state: None,
state: protocol::subscription::SubscriptionWorkerState::Idle,
has_running_internal_workers: false,
workspace_id: Some("workspace-1".to_string()),
display_name: Some("Companion".to_string()),
profile: Some("builtin:companion".to_string()),
workdir_attachments: vec![
protocol::subscription::SubscriptionWorkerWorkdirAttachment {
alias: "external".to_string(),
repository_id: None,
repository_key: None,
working_directory_id: protocol::subscription::SubscriptionWorkdirId::new(
"external-workdir-1",
)
.unwrap(),
},
],
};
assert!(project_repository_keys_with(&mut worker, |_| {
panic!("External Workdir must not require Repository projection")
}));
worker.validate().unwrap();
assert_eq!(worker.workdir_attachments.len(), 1);
assert_eq!(
worker.workdir_attachments[0].working_directory_id.as_str(),
"external-workdir-1"
);
}
async fn run_with_events(events: mpsc::Receiver<protocol::Event>) -> SubscriptionResponse { async fn run_with_events(events: mpsc::Receiver<protocol::Event>) -> SubscriptionResponse {
let (control_outbound, mut control_receiver) = mpsc::channel(1); let (control_outbound, mut control_receiver) = mpsc::channel(1);
let (protocol_outbound, _protocol_receiver) = mpsc::channel(1); let (protocol_outbound, _protocol_receiver) = mpsc::channel(1);