Expose scheduled tasks in plugin details (#32272)

## What changed

- Add optional `scheduledTasks` metadata to `plugin/read` responses for remote
  plugins, including hourly, daily, weekday, and weekly schedules.
- Preserve the distinction between unavailable metadata (`null`) and a catalog
  response containing no scheduled tasks (`[]`). Local plugin details return
  `null`.
- Export the corresponding Rust, JSON Schema, and TypeScript protocol types.

## Testing

- Cover scheduled-task deserialization and propagation through `plugin/read`.
- Verify missing and empty remote metadata remain distinct.

GitOrigin-RevId: 7af223ed0a1bf6d8f054293084a73f34c6d0332d
This commit is contained in:
Abhinav authored and copyberry committed 2026-07-10 21:13:38 +00:00
1 parent bca577d69a
commit 431ad23be4
16 files changed
+589 -2

No files matched your search

@@ -13414,6 +13414,15 @@
}, },
"type": "array" "type": "array"
}, },
"scheduledTasks": {
"items": {
"$ref": "#/definitions/v2/ScheduledTaskSummary"
},
"type": [
"array",
"null"
]
},
"shareUrl": { "shareUrl": {
"type": [ "type": [
"string", "string",
@@ -16407,6 +16416,143 @@
}, },
"type": "object" "type": "object"
}, },
"ScheduledTaskSchedule": {
"oneOf": [
{
"properties": {
"days": {
"items": {
"$ref": "#/definitions/v2/ScheduledTaskWeekday"
},
"type": [
"array",
"null"
]
},
"intervalHours": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"type": {
"enum": [
"hourly"
],
"title": "HourlyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"intervalHours",
"type"
],
"title": "HourlyScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"time": {
"type": "string"
},
"type": {
"enum": [
"daily"
],
"title": "DailyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"time",
"type"
],
"title": "DailyScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"time": {
"type": "string"
},
"type": {
"enum": [
"weekdays"
],
"title": "WeekdaysScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"time",
"type"
],
"title": "WeekdaysScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"days": {
"items": {
"$ref": "#/definitions/v2/ScheduledTaskWeekday"
},
"type": "array"
},
"time": {
"type": "string"
},
"type": {
"enum": [
"weekly"
],
"title": "WeeklyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"days",
"time",
"type"
],
"title": "WeeklyScheduledTaskSchedule",
"type": "object"
}
]
},
"ScheduledTaskSummary": {
"properties": {
"key": {
"type": "string"
},
"name": {
"type": "string"
},
"prompt": {
"type": "string"
},
"schedule": {
"$ref": "#/definitions/v2/ScheduledTaskSchedule"
}
},
"required": [
"key",
"name",
"prompt",
"schedule"
],
"type": "object"
},
"ScheduledTaskWeekday": {
"enum": [
"MO",
"TU",
"WE",
"TH",
"FR",
"SA",
"SU"
],
"type": "string"
},
"SelectedCapabilityRoot": { "SelectedCapabilityRoot": {
"description": "A user-selected root that can expose one or more runtime capabilities.", "description": "A user-selected root that can expose one or more runtime capabilities.",
"properties": { "properties": {
@@ -9818,6 +9818,15 @@
}, },
"type": "array" "type": "array"
}, },
"scheduledTasks": {
"items": {
"$ref": "#/definitions/ScheduledTaskSummary"
},
"type": [
"array",
"null"
]
},
"shareUrl": { "shareUrl": {
"type": [ "type": [
"string", "string",
@@ -12811,6 +12820,143 @@
}, },
"type": "object" "type": "object"
}, },
"ScheduledTaskSchedule": {
"oneOf": [
{
"properties": {
"days": {
"items": {
"$ref": "#/definitions/ScheduledTaskWeekday"
},
"type": [
"array",
"null"
]
},
"intervalHours": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"type": {
"enum": [
"hourly"
],
"title": "HourlyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"intervalHours",
"type"
],
"title": "HourlyScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"time": {
"type": "string"
},
"type": {
"enum": [
"daily"
],
"title": "DailyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"time",
"type"
],
"title": "DailyScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"time": {
"type": "string"
},
"type": {
"enum": [
"weekdays"
],
"title": "WeekdaysScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"time",
"type"
],
"title": "WeekdaysScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"days": {
"items": {
"$ref": "#/definitions/ScheduledTaskWeekday"
},
"type": "array"
},
"time": {
"type": "string"
},
"type": {
"enum": [
"weekly"
],
"title": "WeeklyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"days",
"time",
"type"
],
"title": "WeeklyScheduledTaskSchedule",
"type": "object"
}
]
},
"ScheduledTaskSummary": {
"properties": {
"key": {
"type": "string"
},
"name": {
"type": "string"
},
"prompt": {
"type": "string"
},
"schedule": {
"$ref": "#/definitions/ScheduledTaskSchedule"
}
},
"required": [
"key",
"name",
"prompt",
"schedule"
],
"type": "object"
},
"ScheduledTaskWeekday": {
"enum": [
"MO",
"TU",
"WE",
"TH",
"FR",
"SA",
"SU"
],
"type": "string"
},
"SelectedCapabilityRoot": { "SelectedCapabilityRoot": {
"description": "A user-selected root that can expose one or more runtime capabilities.", "description": "A user-selected root that can expose one or more runtime capabilities.",
"properties": { "properties": {
@@ -192,6 +192,15 @@
}, },
"type": "array" "type": "array"
}, },
"scheduledTasks": {
"items": {
"$ref": "#/definitions/ScheduledTaskSummary"
},
"type": [
"array",
"null"
]
},
"shareUrl": { "shareUrl": {
"type": [ "type": [
"string", "string",
@@ -718,6 +727,143 @@
], ],
"type": "object" "type": "object"
}, },
"ScheduledTaskSchedule": {
"oneOf": [
{
"properties": {
"days": {
"items": {
"$ref": "#/definitions/ScheduledTaskWeekday"
},
"type": [
"array",
"null"
]
},
"intervalHours": {
"format": "uint32",
"minimum": 0.0,
"type": "integer"
},
"type": {
"enum": [
"hourly"
],
"title": "HourlyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"intervalHours",
"type"
],
"title": "HourlyScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"time": {
"type": "string"
},
"type": {
"enum": [
"daily"
],
"title": "DailyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"time",
"type"
],
"title": "DailyScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"time": {
"type": "string"
},
"type": {
"enum": [
"weekdays"
],
"title": "WeekdaysScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"time",
"type"
],
"title": "WeekdaysScheduledTaskSchedule",
"type": "object"
},
{
"properties": {
"days": {
"items": {
"$ref": "#/definitions/ScheduledTaskWeekday"
},
"type": "array"
},
"time": {
"type": "string"
},
"type": {
"enum": [
"weekly"
],
"title": "WeeklyScheduledTaskScheduleType",
"type": "string"
}
},
"required": [
"days",
"time",
"type"
],
"title": "WeeklyScheduledTaskSchedule",
"type": "object"
}
]
},
"ScheduledTaskSummary": {
"properties": {
"key": {
"type": "string"
},
"name": {
"type": "string"
},
"prompt": {
"type": "string"
},
"schedule": {
"$ref": "#/definitions/ScheduledTaskSchedule"
}
},
"required": [
"key",
"name",
"prompt",
"schedule"
],
"type": "object"
},
"ScheduledTaskWeekday": {
"enum": [
"MO",
"TU",
"WE",
"TH",
"FR",
"SA",
"SU"
],
"type": "string"
},
"SkillInterface": { "SkillInterface": {
"properties": { "properties": {
"brandColor": { "brandColor": {
@@ -6,6 +6,7 @@ import type { AppSummary } from "./AppSummary";
import type { AppTemplateSummary } from "./AppTemplateSummary"; import type { AppTemplateSummary } from "./AppTemplateSummary";
import type { PluginHookSummary } from "./PluginHookSummary"; import type { PluginHookSummary } from "./PluginHookSummary";
import type { PluginSummary } from "./PluginSummary"; import type { PluginSummary } from "./PluginSummary";
import type { ScheduledTaskSummary } from "./ScheduledTaskSummary";
import type { SkillSummary } from "./SkillSummary"; import type { SkillSummary } from "./SkillSummary";
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, shareUrl: string | null, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, appTemplates: Array<AppTemplateSummary>, mcpServers: Array<string>, }; export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, shareUrl: string | null, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, appTemplates: Array<AppTemplateSummary>, mcpServers: Array<string>, scheduledTasks: Array<ScheduledTaskSummary> | null, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday";
export type ScheduledTaskSchedule = { "type": "hourly", intervalHours: number, days: Array<ScheduledTaskWeekday> | null, } | { "type": "daily", time: string, } | { "type": "weekdays", time: string, } | { "type": "weekly", days: Array<ScheduledTaskWeekday>, time: string, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ScheduledTaskSchedule } from "./ScheduledTaskSchedule";
export type ScheduledTaskSummary = { key: string, name: string, prompt: string, schedule: ScheduledTaskSchedule, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ScheduledTaskWeekday = "MO" | "TU" | "WE" | "TH" | "FR" | "SA" | "SU";
@@ -361,6 +361,9 @@ export type { ReviewTarget } from "./ReviewTarget";
export type { SandboxMode } from "./SandboxMode"; export type { SandboxMode } from "./SandboxMode";
export type { SandboxPolicy } from "./SandboxPolicy"; export type { SandboxPolicy } from "./SandboxPolicy";
export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite";
export type { ScheduledTaskSchedule } from "./ScheduledTaskSchedule";
export type { ScheduledTaskSummary } from "./ScheduledTaskSummary";
export type { ScheduledTaskWeekday } from "./ScheduledTaskWeekday";
export type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot"; export type { SelectedCapabilityRoot } from "./SelectedCapabilityRoot";
export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams"; export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams";
export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse"; export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse";
@@ -663,6 +663,55 @@ pub struct PluginDetail {
pub apps: Vec<AppSummary>, pub apps: Vec<AppSummary>,
pub app_templates: Vec<AppTemplateSummary>, pub app_templates: Vec<AppTemplateSummary>,
pub mcp_servers: Vec<String>, pub mcp_servers: Vec<String>,
pub scheduled_tasks: Option<Vec<ScheduledTaskSummary>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ScheduledTaskSummary {
pub key: String,
pub name: String,
pub prompt: String,
pub schedule: ScheduledTaskSchedule,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum ScheduledTaskSchedule {
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Hourly {
interval_hours: u32,
days: Option<Vec<ScheduledTaskWeekday>>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Daily { time: String },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Weekdays { time: String },
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Weekly {
days: Vec<ScheduledTaskWeekday>,
time: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[ts(export_to = "v2/")]
pub enum ScheduledTaskWeekday {
Mo,
Tu,
We,
Th,
Fr,
Sa,
Su,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
+1 -1
View File
@@ -215,7 +215,7 @@ Example with notification opt-out:
- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. - `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors.
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, nullable remote install-policy provenance in `installPolicySource` (`WORKSPACE_SETTING` or `IMPLICIT_CANONICAL_APP`), the remote marketplace `version` and locally materialized `localVersion` when available, plugin `availability` (`AVAILABLE` by default or `DISABLED_BY_ADMIN` for remote plugins blocked upstream), fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. Clients can explicitly request the remote `workspace-directory`, `shared-with-me`, or `created-by-me-remote` marketplace kinds. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**).
- `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Remote rows include nullable `installPolicySource`; local rows return `null`. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**). - `plugin/installed` — list installed plugin rows plus any explicitly requested local install-suggestion plugin names, without fetching the broader remote catalog. Remote rows include nullable `installPolicySource`; local rows return `null`. Mention surfaces can use this narrower view when they need plugin mention payloads rather than plugin-page discovery data (**under development; do not call from production clients yet**).
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**). - `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/hooks/apps/MCP server names. Remote plugin details can include scheduled task summaries from the catalog; `scheduledTasks: null` means the metadata is unavailable, while an empty array means the catalog found no scheduled tasks. Remote plugin details expose the canonical `shareUrl` supplied by the remote catalog when available; it is `null` for local plugins or when the catalog omits it. This field is separate from `summary.shareContext`, which continues to describe user and workspace sharing state. Returned plugin skills include their current `enabled` state after local config filtering; bundled hooks are returned as lightweight declaration summaries keyed for correlation with `hooks/list`. Use `plugin/install`'s `appsNeedingAuth` to drive post-install authentication and `app/list`'s `isAccessible` to determine current connector accessibility (**under development; do not call from production clients yet**).
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
- `skills/changed` — notification emitted when watched local skill files change. - `skills/changed` — notification emitted when watched local skill files change.
- `app/list` — list available apps. - `app/list` — list available apps.
@@ -1130,6 +1130,7 @@ impl PluginRequestProcessor {
apps: app_summaries, apps: app_summaries,
app_templates: Vec::new(), app_templates: Vec::new(),
mcp_servers: outcome.plugin.mcp_server_names, mcp_servers: outcome.plugin.mcp_server_names,
scheduled_tasks: None,
} }
} }
Err(remote_marketplace_name) => { Err(remote_marketplace_name) => {
@@ -2288,6 +2289,7 @@ fn remote_plugin_detail_to_info(
apps, apps,
app_templates, app_templates,
mcp_servers: detail.mcp_servers, mcp_servers: detail.mcp_servers,
scheduled_tasks: detail.scheduled_tasks,
} }
} }
@@ -36,6 +36,9 @@ use codex_app_server_protocol::PluginSkillReadParams;
use codex_app_server_protocol::PluginSkillReadResponse; use codex_app_server_protocol::PluginSkillReadResponse;
use codex_app_server_protocol::PluginSource; use codex_app_server_protocol::PluginSource;
use codex_app_server_protocol::RequestId; use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ScheduledTaskSchedule;
use codex_app_server_protocol::ScheduledTaskSummary;
use codex_app_server_protocol::ScheduledTaskWeekday;
use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::AuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -176,6 +179,27 @@ apps = true
"logo_url_dark": "https://example.com/example-plugin-dark.png" "logo_url_dark": "https://example.com/example-plugin-dark.png"
}, },
"skills": [], "skills": [],
"scheduled_tasks": [
{
"key": "weekday-triage",
"name": "Weekday triage",
"prompt": "Triage the support queue.",
"schedule": {
"type": "weekdays",
"time": "08:30"
}
},
{
"key": "queue-monitor",
"name": "Queue monitor",
"prompt": "Check the queue.",
"schedule": {
"type": "hourly",
"intervalHours": 2,
"days": ["MO", "WE", "FR"]
}
}
],
"mcp_servers": [ "mcp_servers": [
{ {
"key": "example-server", "key": "example-server",
@@ -307,6 +331,32 @@ apps = true
response.plugin.mcp_servers, response.plugin.mcp_servers,
vec!["other-server".to_string()] vec!["other-server".to_string()]
); );
assert_eq!(
response.plugin.scheduled_tasks,
Some(vec![
ScheduledTaskSummary {
key: "weekday-triage".to_string(),
name: "Weekday triage".to_string(),
prompt: "Triage the support queue.".to_string(),
schedule: ScheduledTaskSchedule::Weekdays {
time: "08:30".to_string(),
},
},
ScheduledTaskSummary {
key: "queue-monitor".to_string(),
name: "Queue monitor".to_string(),
prompt: "Check the queue.".to_string(),
schedule: ScheduledTaskSchedule::Hourly {
interval_hours: 2,
days: Some(vec![
ScheduledTaskWeekday::Mo,
ScheduledTaskWeekday::We,
ScheduledTaskWeekday::Fr,
]),
},
},
])
);
assert_eq!( assert_eq!(
response response
.plugin .plugin
@@ -975,6 +1025,7 @@ enabled = true
mcp.read_stream_until_response_message(RequestId::Integer(request_id)), mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
) )
.await??; .await??;
assert_eq!(response.result["plugin"]["scheduledTasks"], json!(null));
let response: PluginReadResponse = to_response(response)?; let response: PluginReadResponse = to_response(response)?;
assert_eq!(response.plugin.marketplace_name, "openai-curated"); assert_eq!(response.plugin.marketplace_name, "openai-curated");
+4
View File
@@ -8,6 +8,7 @@ use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInstallPolicySource; use codex_app_server_protocol::PluginInstallPolicySource;
use codex_app_server_protocol::PluginInterface; use codex_app_server_protocol::PluginInterface;
use codex_app_server_protocol::ScheduledTaskSummary;
use codex_app_server_protocol::SkillInterface; use codex_app_server_protocol::SkillInterface;
use codex_login::CodexAuth; use codex_login::CodexAuth;
use codex_login::default_client::build_reqwest_client; use codex_login::default_client::build_reqwest_client;
@@ -209,6 +210,7 @@ pub struct RemotePluginDetail {
pub app_ids: Vec<String>, pub app_ids: Vec<String>,
pub app_templates: Vec<RemoteAppTemplate>, pub app_templates: Vec<RemoteAppTemplate>,
pub mcp_servers: Vec<String>, pub mcp_servers: Vec<String>,
pub scheduled_tasks: Option<Vec<ScheduledTaskSummary>>,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -504,6 +506,7 @@ struct RemotePluginReleaseResponse {
skills: Vec<RemotePluginSkillResponse>, skills: Vec<RemotePluginSkillResponse>,
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
mcp_servers: Vec<RemotePluginMcpServerResponse>, mcp_servers: Vec<RemotePluginMcpServerResponse>,
scheduled_tasks: Option<Vec<ScheduledTaskSummary>>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
@@ -1290,6 +1293,7 @@ async fn build_remote_plugin_detail(
}) })
.collect(), .collect(),
mcp_servers, mcp_servers,
scheduled_tasks: plugin.release.scheduled_tasks,
}) })
} }
+19
View File
@@ -96,6 +96,24 @@ fn unknown_installation_policy_source_maps_to_none() {
assert_eq!(summary.install_policy_source, None); assert_eq!(summary.install_policy_source, None);
} }
#[test]
fn scheduled_task_metadata_distinguishes_unavailable_from_empty() {
let release = serde_json::json!({
"display_name": "Example",
"description": "Example plugin",
"interface": {},
});
let without_metadata: RemotePluginReleaseResponse =
serde_json::from_value(release.clone()).expect("release should deserialize");
assert_eq!(without_metadata.scheduled_tasks, None);
let mut with_empty_metadata = release;
with_empty_metadata["scheduled_tasks"] = serde_json::json!([]);
let with_empty_metadata: RemotePluginReleaseResponse =
serde_json::from_value(with_empty_metadata).expect("release should deserialize");
assert_eq!(with_empty_metadata.scheduled_tasks, Some(Vec::new()));
}
fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem { fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
RemotePluginDirectoryItem { RemotePluginDirectoryItem {
id: id.to_string(), id: id.to_string(),
@@ -138,6 +156,7 @@ fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
}, },
skills: Vec::new(), skills: Vec::new(),
mcp_servers: Vec::new(), mcp_servers: Vec::new(),
scheduled_tasks: None,
}, },
} }
} }
@@ -1503,6 +1503,7 @@ pub(super) fn plugins_test_detail(
.collect(), .collect(),
app_templates: Vec::new(), app_templates: Vec::new(),
mcp_servers: mcp_servers.iter().map(|name| (*name).to_string()).collect(), mcp_servers: mcp_servers.iter().map(|name| (*name).to_string()).collect(),
scheduled_tasks: None,
} }
} }
@@ -1522,6 +1523,7 @@ pub(super) fn plugins_test_remote_detail(
apps: Vec::new(), apps: Vec::new(),
app_templates: Vec::new(), app_templates: Vec::new(),
mcp_servers: Vec::new(), mcp_servers: Vec::new(),
scheduled_tasks: None,
} }
} }
@@ -876,6 +876,7 @@ async fn plugin_detail_unmaterialized_default_uses_remote_install_path() {
apps: Vec::new(), apps: Vec::new(),
app_templates: Vec::new(), app_templates: Vec::new(),
mcp_servers: Vec::new(), mcp_servers: Vec::new(),
scheduled_tasks: None,
}, },
}), }),
); );