Make feed and app lookups self-diagnosing
This commit is contained in:
@@ -193,23 +193,35 @@ pub struct WorkerBackgroundApprovalListResponse {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WorkerFeedListResponse {
|
||||
pub items: Vec<FeedItemRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub state_root: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WorkerFeedItemResponse {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub item: Option<FeedItemRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub state_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WorkerAppListResponse {
|
||||
pub apps: Vec<LibraryAppRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub state_root: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WorkerAppSnapshotResponse {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub app: Option<LibraryAppRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub state_root: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -308,10 +308,15 @@ async fn list_feed(
|
||||
Query(query): Query<FeedQuery>,
|
||||
) -> Result<Json<WorkerFeedListResponse>, StatusCode> {
|
||||
authorize(&headers, &state.config.auth_token)?;
|
||||
let items = ArtifactLibraryStore::new()
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let items = store
|
||||
.list_feed(query.turn_id.as_deref())
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(WorkerFeedListResponse { items }))
|
||||
Ok(Json(WorkerFeedListResponse {
|
||||
items,
|
||||
state_root: store.state_root_display().ok(),
|
||||
warnings: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_feed_item(
|
||||
@@ -320,10 +325,14 @@ async fn get_feed_item(
|
||||
AxumPath(feed_item_id): AxumPath<String>,
|
||||
) -> Result<Json<WorkerFeedItemResponse>, StatusCode> {
|
||||
authorize(&headers, &state.config.auth_token)?;
|
||||
let item = ArtifactLibraryStore::new()
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let item = store
|
||||
.get_feed_item(&feed_item_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(WorkerFeedItemResponse { item }))
|
||||
Ok(Json(WorkerFeedItemResponse {
|
||||
item,
|
||||
state_root: store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_feed_file(
|
||||
@@ -454,10 +463,15 @@ async fn list_apps(
|
||||
headers: HeaderMap,
|
||||
) -> Result<Json<WorkerAppListResponse>, StatusCode> {
|
||||
authorize(&headers, &state.config.auth_token)?;
|
||||
let apps = ArtifactLibraryStore::new()
|
||||
.list_apps()
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let (apps, warnings) = store
|
||||
.list_apps_with_warnings()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(WorkerAppListResponse { apps }))
|
||||
Ok(Json(WorkerAppListResponse {
|
||||
apps,
|
||||
state_root: store.state_root_display().ok(),
|
||||
warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_app(
|
||||
@@ -466,10 +480,14 @@ async fn get_app(
|
||||
AxumPath(app_id): AxumPath<String>,
|
||||
) -> Result<Json<WorkerAppSnapshotResponse>, StatusCode> {
|
||||
authorize(&headers, &state.config.auth_token)?;
|
||||
let app = ArtifactLibraryStore::new()
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let app = store
|
||||
.get_app(&app_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(WorkerAppSnapshotResponse { app }))
|
||||
Ok(Json(WorkerAppSnapshotResponse {
|
||||
app,
|
||||
state_root: store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_app_version(
|
||||
@@ -503,10 +521,14 @@ async fn launch_app(
|
||||
AxumPath(app_id): AxumPath<String>,
|
||||
) -> Result<Json<WorkerAppSnapshotResponse>, StatusCode> {
|
||||
authorize(&headers, &state.config.auth_token)?;
|
||||
let app = ArtifactLibraryStore::new()
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let app = store
|
||||
.mark_app_launched(&app_id)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
Ok(Json(WorkerAppSnapshotResponse { app }))
|
||||
Ok(Json(WorkerAppSnapshotResponse {
|
||||
app,
|
||||
state_root: store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn open_app_workspace(
|
||||
|
||||
@@ -211,6 +211,54 @@ pub struct LibraryAppRecord {
|
||||
pub current_version: LibraryAppVersionRecord,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct FeedLookupDiagnostics {
|
||||
pub state_root: String,
|
||||
pub feed_item_id: String,
|
||||
pub item_record_path: String,
|
||||
pub item_record_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub item_record_error: Option<String>,
|
||||
pub payload_dir: String,
|
||||
pub payload_dir_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub resolved_file_path: Option<String>,
|
||||
pub resolved_file_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub resolved_file_error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AppLookupDiagnostics {
|
||||
pub state_root: String,
|
||||
pub app_id: String,
|
||||
pub app_dir: String,
|
||||
pub app_dir_exists: bool,
|
||||
pub host_manifest_path: String,
|
||||
pub host_manifest_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub host_manifest_kind: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub host_manifest_error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_version_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_version_dir: Option<String>,
|
||||
pub current_version_dir_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub version_record_path: Option<String>,
|
||||
pub version_record_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub version_record_error: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bundle_manifest_path: Option<String>,
|
||||
pub bundle_manifest_exists: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bundle_manifest_error: Option<String>,
|
||||
pub repo_path: String,
|
||||
pub repo_exists: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AppPackageRequest {
|
||||
pub feed_item_id: String,
|
||||
@@ -356,6 +404,132 @@ impl ArtifactLibraryStore {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn state_root_display(&self) -> io::Result<String> {
|
||||
Ok(state_root()?.display().to_string())
|
||||
}
|
||||
|
||||
pub fn feed_lookup_diagnostics(&self, feed_item_id: &str) -> io::Result<FeedLookupDiagnostics> {
|
||||
let state_root_path = state_root()?;
|
||||
let sanitized_feed_item_id = sanitize_state_component(feed_item_id);
|
||||
let item_record_path = feed_item_path(&sanitized_feed_item_id)?;
|
||||
let payload_dir = feed_payload_dir(&sanitized_feed_item_id)?;
|
||||
let (item_record_exists, item_record_error, item) = match read_json::<FeedItemRecord>(&item_record_path) {
|
||||
Ok(item) => (item_record_path.exists(), None, item),
|
||||
Err(error) => (item_record_path.exists(), Some(error.to_string()), None),
|
||||
};
|
||||
let (resolved_file_path, resolved_file_exists, resolved_file_error) = if item.is_some() {
|
||||
match self.feed_file_path(&sanitized_feed_item_id) {
|
||||
Ok(Some(path)) => (Some(path.display().to_string()), path.exists(), None),
|
||||
Ok(None) => (None, false, None),
|
||||
Err(error) => (None, false, Some(error.to_string())),
|
||||
}
|
||||
} else {
|
||||
(None, false, None)
|
||||
};
|
||||
Ok(FeedLookupDiagnostics {
|
||||
state_root: state_root_path.display().to_string(),
|
||||
feed_item_id: feed_item_id.to_string(),
|
||||
item_record_path: item_record_path.display().to_string(),
|
||||
item_record_exists,
|
||||
item_record_error,
|
||||
payload_dir: payload_dir.display().to_string(),
|
||||
payload_dir_exists: payload_dir.exists(),
|
||||
resolved_file_path,
|
||||
resolved_file_exists,
|
||||
resolved_file_error,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn app_lookup_diagnostics(&self, app_id: &str) -> io::Result<AppLookupDiagnostics> {
|
||||
let state_root_path = state_root()?;
|
||||
let sanitized_app_id = sanitize_state_component(app_id);
|
||||
let app_dir_path = app_dir(&sanitized_app_id)?;
|
||||
let host_manifest_path = app_manifest_path(&sanitized_app_id)?;
|
||||
let repo_path = app_repo_dir(&sanitized_app_id)?;
|
||||
|
||||
let mut host_manifest_kind = None;
|
||||
let mut host_manifest_error = None;
|
||||
let mut current_version_id = None;
|
||||
match read_host_manifest(&sanitized_app_id) {
|
||||
Ok(Some(StoredHostManifest::V2(manifest))) => {
|
||||
host_manifest_kind = Some(String::from("v2"));
|
||||
current_version_id = Some(manifest.current_version_id);
|
||||
}
|
||||
Ok(Some(StoredHostManifest::V1(legacy))) => {
|
||||
host_manifest_kind = Some(String::from("v1_legacy"));
|
||||
current_version_id = Some(legacy.current_version_id);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => host_manifest_error = Some(error.to_string()),
|
||||
}
|
||||
|
||||
let current_version_dir = current_version_id
|
||||
.as_deref()
|
||||
.map(|version_id| app_version_dir(&sanitized_app_id, version_id))
|
||||
.transpose()?;
|
||||
let version_record_path = current_version_id
|
||||
.as_deref()
|
||||
.map(|version_id| app_version_record_path(&sanitized_app_id, version_id))
|
||||
.transpose()?;
|
||||
let bundle_manifest_path = current_version_id
|
||||
.as_deref()
|
||||
.map(|version_id| app_version_bundle_dir(&sanitized_app_id, version_id).map(|path| app_bundle_manifest_path(&path)))
|
||||
.transpose()?;
|
||||
|
||||
let version_record_exists = version_record_path
|
||||
.as_ref()
|
||||
.is_some_and(|path| path.exists());
|
||||
let bundle_manifest_exists = bundle_manifest_path
|
||||
.as_ref()
|
||||
.is_some_and(|path| path.exists());
|
||||
let current_version_dir_exists = current_version_dir
|
||||
.as_ref()
|
||||
.is_some_and(|path| path.exists());
|
||||
|
||||
let version_record_error = match version_record_path.as_ref() {
|
||||
Some(path) => match read_json::<LibraryAppVersionRecord>(path) {
|
||||
Ok(_) => None,
|
||||
Err(error) => Some(error.to_string()),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let bundle_manifest_error = match current_version_dir.as_ref() {
|
||||
Some(path) => match read_bundle_manifest(&path.join("bundle")) {
|
||||
Ok(_) => None,
|
||||
Err(error) => Some(error.to_string()),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(AppLookupDiagnostics {
|
||||
state_root: state_root_path.display().to_string(),
|
||||
app_id: app_id.to_string(),
|
||||
app_dir: app_dir_path.display().to_string(),
|
||||
app_dir_exists: app_dir_path.exists(),
|
||||
host_manifest_path: host_manifest_path.display().to_string(),
|
||||
host_manifest_exists: host_manifest_path.exists(),
|
||||
host_manifest_kind,
|
||||
host_manifest_error,
|
||||
current_version_id: current_version_id.clone(),
|
||||
current_version_dir: current_version_dir
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string()),
|
||||
current_version_dir_exists,
|
||||
version_record_path: version_record_path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string()),
|
||||
version_record_exists,
|
||||
version_record_error,
|
||||
bundle_manifest_path: bundle_manifest_path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string()),
|
||||
bundle_manifest_exists,
|
||||
bundle_manifest_error,
|
||||
repo_path: repo_path.display().to_string(),
|
||||
repo_exists: repo_path.exists(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn capture_workspace_snapshot(root: &Path) -> io::Result<WorkspaceSnapshot> {
|
||||
let mut files = BTreeMap::new();
|
||||
if !root.exists() {
|
||||
@@ -709,11 +883,12 @@ impl ArtifactLibraryStore {
|
||||
Ok(Some(AppPackageResult { app, created }))
|
||||
}
|
||||
|
||||
pub fn list_apps(&self) -> io::Result<Vec<LibraryAppRecord>> {
|
||||
pub fn list_apps_with_warnings(&self) -> io::Result<(Vec<LibraryAppRecord>, Vec<String>)> {
|
||||
let mut apps = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let entries = match fs::read_dir(library_apps_dir()?) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(apps),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok((apps, warnings)),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
for entry in entries {
|
||||
@@ -730,10 +905,12 @@ impl ArtifactLibraryStore {
|
||||
}
|
||||
Ok(Some(_)) | Ok(None) => {}
|
||||
Err(error) => {
|
||||
eprintln!(
|
||||
"artifact library: skipping unreadable app `{}` while listing apps: {}",
|
||||
let warning = format!(
|
||||
"skipping unreadable app `{}` while listing apps: {}",
|
||||
app_id, error
|
||||
);
|
||||
eprintln!("artifact library: {warning}");
|
||||
warnings.push(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -744,7 +921,11 @@ impl ArtifactLibraryStore {
|
||||
.cmp(&left.manifest.last_launched_at)
|
||||
.then_with(|| left.manifest.title.cmp(&right.manifest.title))
|
||||
});
|
||||
Ok(apps)
|
||||
Ok((apps, warnings))
|
||||
}
|
||||
|
||||
pub fn list_apps(&self) -> io::Result<Vec<LibraryAppRecord>> {
|
||||
self.list_apps_with_warnings().map(|(apps, _warnings)| apps)
|
||||
}
|
||||
|
||||
pub fn get_app(&self, app_id: &str) -> io::Result<Option<LibraryAppRecord>> {
|
||||
@@ -2080,4 +2261,56 @@ mod tests {
|
||||
std::env::remove_var("CLAW_WORKER_DEFAULT_CWD");
|
||||
let _ = std::fs::remove_dir_all(&state_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_lookup_diagnostics_report_broken_manifest() {
|
||||
let _lock = test_env_lock();
|
||||
let state_root =
|
||||
std::env::temp_dir().join(format!("artifact-lib-diag-app-{}", super::now_secs()));
|
||||
let workspace = state_root.join("workspace");
|
||||
let _ = std::fs::remove_dir_all(&state_root);
|
||||
std::fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
std::env::set_var("CLAW_WORKER_STATE_ROOT", &state_root);
|
||||
std::env::set_var("CLAW_WORKER_DEFAULT_CWD", &workspace);
|
||||
|
||||
let broken_dir = super::library_apps_dir()
|
||||
.expect("library apps dir")
|
||||
.join("broken-app");
|
||||
std::fs::create_dir_all(&broken_dir).expect("create broken dir");
|
||||
std::fs::write(broken_dir.join("manifest.json"), "{not-json").expect("write broken manifest");
|
||||
|
||||
let diagnostics = ArtifactLibraryStore::new()
|
||||
.app_lookup_diagnostics("broken-app")
|
||||
.expect("diagnostics should load");
|
||||
assert!(diagnostics.app_dir_exists);
|
||||
assert!(diagnostics.host_manifest_exists);
|
||||
assert!(diagnostics.host_manifest_error.is_some());
|
||||
|
||||
std::env::remove_var("CLAW_WORKER_STATE_ROOT");
|
||||
std::env::remove_var("CLAW_WORKER_DEFAULT_CWD");
|
||||
let _ = std::fs::remove_dir_all(&state_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feed_lookup_diagnostics_report_missing_item_paths() {
|
||||
let _lock = test_env_lock();
|
||||
let state_root =
|
||||
std::env::temp_dir().join(format!("artifact-lib-diag-feed-{}", super::now_secs()));
|
||||
let workspace = state_root.join("workspace");
|
||||
let _ = std::fs::remove_dir_all(&state_root);
|
||||
std::fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
std::env::set_var("CLAW_WORKER_STATE_ROOT", &state_root);
|
||||
std::env::set_var("CLAW_WORKER_DEFAULT_CWD", &workspace);
|
||||
|
||||
let diagnostics = ArtifactLibraryStore::new()
|
||||
.feed_lookup_diagnostics("missing-feed")
|
||||
.expect("diagnostics should load");
|
||||
assert!(!diagnostics.item_record_exists);
|
||||
assert!(!diagnostics.payload_dir_exists);
|
||||
assert!(diagnostics.item_record_path.ends_with("missing-feed.json"));
|
||||
|
||||
std::env::remove_var("CLAW_WORKER_STATE_ROOT");
|
||||
std::env::remove_var("CLAW_WORKER_DEFAULT_CWD");
|
||||
let _ = std::fs::remove_dir_all(&state_root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,12 @@ pub use background_approval_store::{
|
||||
};
|
||||
pub use artifact_library_store::{
|
||||
AppBundleManifestV1, AppCapability, AppHistoryEntry, AppHistoryResponse, AppKind,
|
||||
AppPackageRequest, AppPackageResult, AppPublishRequest, AppPublishResult, AppRepoLink,
|
||||
AppVisibility, AppWorkspaceRecord, ArtifactLibraryStore, FeedFileInput,
|
||||
FeedItemChangeKind, FeedItemKind, FeedItemRecord, FeedItemSource, FeedItemSourceKind,
|
||||
FeedToAppLink, LibraryAppManifestV2, LibraryAppRecord, LibraryAppVersionRecord,
|
||||
WorkspaceChange, WorkspaceSnapshot, WorkspaceSnapshotEntry,
|
||||
AppLookupDiagnostics, AppPackageRequest, AppPackageResult, AppPublishRequest,
|
||||
AppPublishResult, AppRepoLink, AppVisibility, AppWorkspaceRecord, ArtifactLibraryStore,
|
||||
FeedFileInput, FeedItemChangeKind, FeedItemKind, FeedItemRecord, FeedItemSource,
|
||||
FeedItemSourceKind, FeedLookupDiagnostics, FeedToAppLink, LibraryAppManifestV2,
|
||||
LibraryAppRecord, LibraryAppVersionRecord, WorkspaceChange, WorkspaceSnapshot,
|
||||
WorkspaceSnapshotEntry,
|
||||
};
|
||||
pub use bootstrap::{BootstrapPhase, BootstrapPlan};
|
||||
pub use compact::{
|
||||
|
||||
+211
-18
@@ -1515,7 +1515,8 @@ fn run_feed_list(_input: Value) -> Result<String, String> {
|
||||
.collect();
|
||||
to_pretty_json(json!({
|
||||
"items": items,
|
||||
"count": items.len()
|
||||
"count": items.len(),
|
||||
"state_root": store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1524,8 +1525,33 @@ fn run_feed_get(input: FeedGetInput) -> Result<String, String> {
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let item = store
|
||||
.get_feed_item(&input.feed_item_id)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or_else(|| format!("feed item not found: {}", input.feed_item_id))?;
|
||||
.map_err(|error| {
|
||||
let diagnostics = store
|
||||
.feed_lookup_diagnostics(&input.feed_item_id)
|
||||
.ok();
|
||||
diagnostic_error(
|
||||
format!("failed to read feed item: {}", input.feed_item_id).as_str(),
|
||||
json!({
|
||||
"feed_item_id": input.feed_item_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": diagnostics,
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
let diagnostics = store
|
||||
.feed_lookup_diagnostics(&input.feed_item_id)
|
||||
.ok();
|
||||
diagnostic_error(
|
||||
format!("feed item not found: {}", input.feed_item_id).as_str(),
|
||||
json!({
|
||||
"feed_item_id": input.feed_item_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": diagnostics,
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
let linked_app = item
|
||||
.linked_app_id
|
||||
.as_deref()
|
||||
@@ -1536,12 +1562,15 @@ fn run_feed_get(input: FeedGetInput) -> Result<String, String> {
|
||||
to_pretty_json(json!({
|
||||
"item": item,
|
||||
"linked_app": linked_app,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn run_app_list(_input: Value) -> Result<String, String> {
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let apps = store.list_apps().map_err(|error| error.to_string())?;
|
||||
let (apps, warnings) = store
|
||||
.list_apps_with_warnings()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let apps: Vec<_> = apps
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -1563,7 +1592,9 @@ fn run_app_list(_input: Value) -> Result<String, String> {
|
||||
.collect();
|
||||
to_pretty_json(json!({
|
||||
"apps": apps,
|
||||
"count": apps.len()
|
||||
"count": apps.len(),
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"warnings": warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1572,8 +1603,29 @@ fn run_app_get(input: AppGetInput) -> Result<String, String> {
|
||||
let store = ArtifactLibraryStore::new();
|
||||
let app = store
|
||||
.get_app(&input.app_id)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or_else(|| format!("app not found: {}", input.app_id))?;
|
||||
.map_err(|error| {
|
||||
let diagnostics = store.app_lookup_diagnostics(&input.app_id).ok();
|
||||
diagnostic_error(
|
||||
format!("failed to read app: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": diagnostics,
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
let diagnostics = store.app_lookup_diagnostics(&input.app_id).ok();
|
||||
diagnostic_error(
|
||||
format!("app not found: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": diagnostics,
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
let linked_feed_items: Vec<_> = app
|
||||
.manifest
|
||||
.source_feed_item_ids
|
||||
@@ -1591,6 +1643,7 @@ fn run_app_get(input: AppGetInput) -> Result<String, String> {
|
||||
"linked_feed_items": linked_feed_items,
|
||||
"history": history,
|
||||
"workspace": workspace,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1599,19 +1652,42 @@ fn run_app_create_from_feed(input: AppCreateFromFeedInput) -> Result<String, Str
|
||||
let store = ArtifactLibraryStore::new();
|
||||
match store
|
||||
.package_feed_item(AppPackageRequest {
|
||||
feed_item_id: input.feed_item_id,
|
||||
feed_item_id: input.feed_item_id.clone(),
|
||||
requested_app_id: input.app_id,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
})
|
||||
.map_err(|error| error.to_string())?
|
||||
.map_err(|error| {
|
||||
let diagnostics = store
|
||||
.feed_lookup_diagnostics(&input.feed_item_id)
|
||||
.ok();
|
||||
diagnostic_error(
|
||||
format!("failed to create app from feed item: {}", input.feed_item_id).as_str(),
|
||||
json!({
|
||||
"feed_item_id": input.feed_item_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": diagnostics,
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
{
|
||||
Some(result) => to_pretty_json(json!({
|
||||
"created": result.created,
|
||||
"app": result.app,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
})),
|
||||
None => Err(String::from(
|
||||
"feed item could not be packaged into an app",
|
||||
None => Err(diagnostic_error(
|
||||
format!(
|
||||
"feed item could not be packaged into an app: {}",
|
||||
input.feed_item_id
|
||||
)
|
||||
.as_str(),
|
||||
json!({
|
||||
"feed_item_id": input.feed_item_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.feed_lookup_diagnostics(&input.feed_item_id).ok(),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1621,7 +1697,17 @@ fn run_app_open_workspace(input: AppIdInput) -> Result<String, String> {
|
||||
let store = ArtifactLibraryStore::new();
|
||||
match store
|
||||
.open_workspace(&input.app_id)
|
||||
.map_err(|error| error.to_string())?
|
||||
.map_err(|error| {
|
||||
diagnostic_error(
|
||||
format!("failed to open app workspace: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.app_lookup_diagnostics(&input.app_id).ok(),
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
{
|
||||
Some(workspace) => {
|
||||
persist_app_workspace_context(&AppWorkspaceContext {
|
||||
@@ -1641,9 +1727,17 @@ fn run_app_open_workspace(input: AppIdInput) -> Result<String, String> {
|
||||
.map_err(|error| error.to_string())?
|
||||
.display()
|
||||
.to_string(),
|
||||
"state_root": store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
None => Err(format!("app not found: {}", input.app_id)),
|
||||
None => Err(diagnostic_error(
|
||||
format!("app not found: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.app_lookup_diagnostics(&input.app_id).ok(),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1657,7 +1751,17 @@ fn run_app_publish(input: AppPublishInput) -> Result<String, String> {
|
||||
message: input.message,
|
||||
},
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
.map_err(|error| {
|
||||
diagnostic_error(
|
||||
format!("failed to publish app workspace: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.app_lookup_diagnostics(&input.app_id).ok(),
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
{
|
||||
Some(result) => {
|
||||
persist_app_workspace_context(&AppWorkspaceContext {
|
||||
@@ -1671,9 +1775,20 @@ fn run_app_publish(input: AppPublishInput) -> Result<String, String> {
|
||||
.as_secs(),
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
to_pretty_json(result)
|
||||
to_pretty_json(json!({
|
||||
"app": result.app,
|
||||
"workspace": result.workspace,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
None => Err(format!("app not found: {}", input.app_id)),
|
||||
None => Err(diagnostic_error(
|
||||
format!("app not found: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.app_lookup_diagnostics(&input.app_id).ok(),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1682,7 +1797,17 @@ fn run_app_archive(input: AppIdInput) -> Result<String, String> {
|
||||
let store = ArtifactLibraryStore::new();
|
||||
match store
|
||||
.archive_app(&input.app_id)
|
||||
.map_err(|error| error.to_string())?
|
||||
.map_err(|error| {
|
||||
diagnostic_error(
|
||||
format!("failed to archive app: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.app_lookup_diagnostics(&input.app_id).ok(),
|
||||
"error": error.to_string(),
|
||||
}),
|
||||
)
|
||||
})?
|
||||
{
|
||||
Some(app) => {
|
||||
if load_app_workspace_context()
|
||||
@@ -1694,9 +1819,17 @@ fn run_app_archive(input: AppIdInput) -> Result<String, String> {
|
||||
to_pretty_json(json!({
|
||||
"archived": true,
|
||||
"app": app,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
}))
|
||||
}
|
||||
None => Err(format!("app not found: {}", input.app_id)),
|
||||
None => Err(diagnostic_error(
|
||||
format!("app not found: {}", input.app_id).as_str(),
|
||||
json!({
|
||||
"app_id": input.app_id,
|
||||
"state_root": store.state_root_display().ok(),
|
||||
"diagnostics": store.app_lookup_diagnostics(&input.app_id).ok(),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2406,6 +2539,13 @@ fn to_pretty_json<T: serde::Serialize>(value: T) -> Result<String, String> {
|
||||
serde_json::to_string_pretty(&value).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn diagnostic_error(summary: &str, details: Value) -> String {
|
||||
match serde_json::to_string_pretty(&details) {
|
||||
Ok(rendered) => format!("{summary}\n{rendered}"),
|
||||
Err(error) => format!("{summary} ({error})"),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn io_to_string(error: std::io::Error) -> String {
|
||||
error.to_string()
|
||||
@@ -6200,6 +6340,7 @@ mod tests {
|
||||
let listed_json: serde_json::Value =
|
||||
serde_json::from_str(&listed).expect("listed json");
|
||||
assert_eq!(listed_json["count"], 1);
|
||||
assert!(listed_json["state_root"].as_str().is_some());
|
||||
|
||||
let opened = execute_tool(
|
||||
"AppOpenWorkspace",
|
||||
@@ -6264,6 +6405,7 @@ mod tests {
|
||||
.as_array()
|
||||
.is_some_and(|entries| entries.len() >= 2)
|
||||
);
|
||||
assert!(fetched_json["state_root"].as_str().is_some());
|
||||
|
||||
let archived = execute_tool(
|
||||
"AppArchive",
|
||||
@@ -6293,6 +6435,57 @@ mod tests {
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_tools_report_lookup_diagnostics_for_missing_ids() {
|
||||
let _guard = env_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let state_root = temp_path("app-diag-state");
|
||||
let workspace_root = temp_path("app-diag-workspace");
|
||||
fs::create_dir_all(&state_root).expect("create state root");
|
||||
fs::create_dir_all(&workspace_root).expect("create workspace root");
|
||||
|
||||
let old_state_root = std::env::var_os("CLAW_WORKER_STATE_ROOT");
|
||||
let old_default_cwd = std::env::var_os("CLAW_WORKER_DEFAULT_CWD");
|
||||
std::env::set_var("CLAW_WORKER_STATE_ROOT", &state_root);
|
||||
std::env::set_var("CLAW_WORKER_DEFAULT_CWD", &workspace_root);
|
||||
|
||||
let result = (|| {
|
||||
let error = execute_tool(
|
||||
"AppGet",
|
||||
&json!({
|
||||
"app_id": "missing-app",
|
||||
}),
|
||||
)
|
||||
.expect_err("missing app should report diagnostics");
|
||||
assert!(error.contains("\"app_id\": \"missing-app\""));
|
||||
assert!(error.contains("\"state_root\":"));
|
||||
assert!(error.contains("\"app_dir_exists\": false"));
|
||||
|
||||
let error = execute_tool(
|
||||
"FeedGet",
|
||||
&json!({
|
||||
"feed_item_id": "missing-feed",
|
||||
}),
|
||||
)
|
||||
.expect_err("missing feed should report diagnostics");
|
||||
assert!(error.contains("\"feed_item_id\": \"missing-feed\""));
|
||||
assert!(error.contains("\"item_record_exists\": false"));
|
||||
})();
|
||||
|
||||
match old_state_root {
|
||||
Some(value) => std::env::set_var("CLAW_WORKER_STATE_ROOT", value),
|
||||
None => std::env::remove_var("CLAW_WORKER_STATE_ROOT"),
|
||||
}
|
||||
match old_default_cwd {
|
||||
Some(value) => std::env::set_var("CLAW_WORKER_DEFAULT_CWD", value),
|
||||
None => std::env::remove_var("CLAW_WORKER_DEFAULT_CWD"),
|
||||
}
|
||||
let _ = fs::remove_dir_all(&state_root);
|
||||
let _ = fs::remove_dir_all(&workspace_root);
|
||||
result
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_tool_names() {
|
||||
let error = execute_tool("nope", &json!({})).expect_err("tool should be rejected");
|
||||
|
||||
Reference in New Issue
Block a user