ticket: split ticket feature access presets
This commit is contained in:
parent
a40d9c2027
commit
2c01e7672b
|
|
@ -128,8 +128,12 @@ impl FeatureFlagConfigPartial {
|
|||
pub struct TicketFeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
/// Legacy access field. Prefer `preset` for new profile/DCDL authoring.
|
||||
#[serde(default)]
|
||||
pub access: Option<TicketFeatureAccessConfig>,
|
||||
/// Semantic Ticket access preset for profile/DCDL authoring.
|
||||
#[serde(default)]
|
||||
pub preset: Option<TicketFeatureAccessConfig>,
|
||||
}
|
||||
|
||||
impl TicketFeatureConfigPartial {
|
||||
|
|
@ -137,6 +141,7 @@ impl TicketFeatureConfigPartial {
|
|||
Self {
|
||||
enabled: other.enabled.or(self.enabled),
|
||||
access: other.access.or(self.access),
|
||||
preset: other.preset.or(self.preset),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +195,7 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
|
|||
fn from(value: TicketFeatureConfigPartial) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled.unwrap_or_default(),
|
||||
access: value.access.unwrap_or_default(),
|
||||
access: value.preset.or(value.access).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +205,7 @@ impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
|
|||
Self {
|
||||
enabled: Some(value.enabled),
|
||||
access: Some(value.access),
|
||||
preset: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,6 +180,12 @@ impl Default for TicketFeatureConfig {
|
|||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TicketFeatureAccessConfig {
|
||||
ReadOnly,
|
||||
WorkspaceAuthoring,
|
||||
Intake,
|
||||
OrchestrationControl,
|
||||
WorkReport,
|
||||
Review,
|
||||
/// Legacy broad mutation preset retained as a migration shim.
|
||||
Lifecycle,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ use crate::model::{AuthRef, ModelManifest};
|
|||
use crate::plugin::PluginConfig;
|
||||
use crate::{
|
||||
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, ResolveError,
|
||||
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig,
|
||||
WorkerMetaConfig, paths,
|
||||
ScopeConfig, ScopeRule, SkillsConfig, TicketFeatureAccessConfig, WebConfig, WorkerManifest,
|
||||
WorkerManifestConfig, WorkerMetaConfig, paths,
|
||||
};
|
||||
|
||||
const PROFILE_FORMAT_V1: &str = "yoi.profile.v1";
|
||||
|
|
@ -1012,6 +1012,18 @@ fn apply_role_profile(
|
|||
value["feature"]["memory"] = serde_json::json!({ "enabled": memory });
|
||||
value["feature"]["web"] = serde_json::json!({ "enabled": web });
|
||||
value["feature"]["workers"] = serde_json::json!({ "enabled": workers });
|
||||
let ticket_access = match slug {
|
||||
"companion" => TicketFeatureAccessConfig::WorkspaceAuthoring,
|
||||
"intake" => TicketFeatureAccessConfig::Intake,
|
||||
"orchestrator" => TicketFeatureAccessConfig::OrchestrationControl,
|
||||
"coder" => TicketFeatureAccessConfig::WorkReport,
|
||||
"reviewer" => TicketFeatureAccessConfig::Review,
|
||||
_ => TicketFeatureAccessConfig::Lifecycle,
|
||||
};
|
||||
value["feature"]["ticket"] = serde_json::json!({
|
||||
"enabled": true,
|
||||
"preset": ticket_access,
|
||||
});
|
||||
value["feature"]["ticket_orchestration"] =
|
||||
serde_json::json!({ "enabled": ticket_orchestration });
|
||||
}
|
||||
|
|
@ -1439,6 +1451,10 @@ mod tests {
|
|||
assert_eq!(companion.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||
assert!(companion.web.is_some());
|
||||
assert!(companion.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
companion.feature.ticket.access,
|
||||
TicketFeatureAccessConfig::WorkspaceAuthoring
|
||||
);
|
||||
assert!(!companion.feature.ticket_orchestration.enabled);
|
||||
assert_eq!(
|
||||
companion.compaction.as_ref().unwrap().threshold,
|
||||
|
|
@ -1461,6 +1477,10 @@ mod tests {
|
|||
assert!(intake.feature.task.enabled);
|
||||
assert!(!intake.feature.workers.enabled);
|
||||
assert!(intake.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
intake.feature.ticket.access,
|
||||
TicketFeatureAccessConfig::Intake
|
||||
);
|
||||
assert!(intake.scope.allow.is_empty());
|
||||
assert!(intake.delegation_scope.allow.is_empty());
|
||||
assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||
|
|
@ -1472,6 +1492,10 @@ mod tests {
|
|||
assert!(orchestrator.feature.task.enabled);
|
||||
assert!(orchestrator.feature.workers.enabled);
|
||||
assert!(orchestrator.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
orchestrator.feature.ticket.access,
|
||||
TicketFeatureAccessConfig::OrchestrationControl
|
||||
);
|
||||
assert!(orchestrator.feature.ticket_orchestration.enabled);
|
||||
assert!(orchestrator.scope.allow.is_empty());
|
||||
assert!(orchestrator.delegation_scope.allow.is_empty());
|
||||
|
|
@ -1491,12 +1515,20 @@ mod tests {
|
|||
assert!(coder.web.is_some());
|
||||
assert!(coder.compaction.is_some());
|
||||
assert!(coder.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
coder.feature.ticket.access,
|
||||
TicketFeatureAccessConfig::WorkReport
|
||||
);
|
||||
assert!(!coder.feature.ticket_orchestration.enabled);
|
||||
|
||||
let reviewer = resolve("reviewer");
|
||||
assert!(reviewer.feature.task.enabled);
|
||||
assert!(!reviewer.feature.workers.enabled);
|
||||
assert!(reviewer.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
reviewer.feature.ticket.access,
|
||||
TicketFeatureAccessConfig::Review
|
||||
);
|
||||
assert!(!reviewer.feature.ticket_orchestration.enabled);
|
||||
assert!(reviewer.scope.allow.is_empty());
|
||||
assert!(reviewer.delegation_scope.allow.is_empty());
|
||||
|
|
|
|||
|
|
@ -510,6 +510,21 @@ impl NewTicket {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TicketItemEdit {
|
||||
pub title: Option<String>,
|
||||
pub body: Option<MarkdownText>,
|
||||
pub author: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TicketDependencyCheck {
|
||||
pub ticket: TicketSummary,
|
||||
pub blockers: Vec<TicketRelationBlocker>,
|
||||
pub queue_guard: TicketQueueGuard,
|
||||
pub recommended_action: TicketWorkspaceNextAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TicketListState {
|
||||
|
|
@ -1391,6 +1406,8 @@ pub trait TicketBackend {
|
|||
fn list(&self, filter: TicketListQuery) -> Result<Vec<TicketSummary>>;
|
||||
fn show(&self, id: TicketIdOrSlug) -> Result<Ticket>;
|
||||
fn create(&self, input: NewTicket) -> Result<TicketRef>;
|
||||
fn edit_item(&self, id: TicketIdOrSlug, edit: TicketItemEdit) -> Result<Ticket>;
|
||||
fn dependency_check(&self, id: TicketIdOrSlug) -> Result<TicketDependencyCheck>;
|
||||
fn add_event(&self, id: TicketIdOrSlug, event: NewTicketEvent) -> Result<()>;
|
||||
fn add_state_changed(&self, id: TicketIdOrSlug, change: TicketStateChange) -> Result<()>;
|
||||
fn add_intake_summary(&self, id: TicketIdOrSlug, summary: TicketIntakeSummary) -> Result<()>;
|
||||
|
|
@ -1449,6 +1466,13 @@ pub enum TicketBackendOperation {
|
|||
Create {
|
||||
input: NewTicket,
|
||||
},
|
||||
EditItem {
|
||||
id: TicketIdOrSlug,
|
||||
edit: TicketItemEdit,
|
||||
},
|
||||
DependencyCheck {
|
||||
id: TicketIdOrSlug,
|
||||
},
|
||||
AddEvent {
|
||||
id: TicketIdOrSlug,
|
||||
event: NewTicketEvent,
|
||||
|
|
@ -1517,6 +1541,7 @@ pub enum TicketBackendOperationResult {
|
|||
Tickets(Vec<TicketSummary>),
|
||||
Ticket(Ticket),
|
||||
TicketRef(TicketRef),
|
||||
DependencyCheck(TicketDependencyCheck),
|
||||
Relation(TicketRelation),
|
||||
Relations(Vec<TicketRelation>),
|
||||
RelationView(TicketRelationView),
|
||||
|
|
@ -1547,6 +1572,12 @@ where
|
|||
TicketBackendOperation::Create { input } => {
|
||||
TicketBackendOperationResult::TicketRef(backend.create(input)?)
|
||||
}
|
||||
TicketBackendOperation::EditItem { id, edit } => {
|
||||
TicketBackendOperationResult::Ticket(backend.edit_item(id, edit)?)
|
||||
}
|
||||
TicketBackendOperation::DependencyCheck { id } => {
|
||||
TicketBackendOperationResult::DependencyCheck(backend.dependency_check(id)?)
|
||||
}
|
||||
TicketBackendOperation::AddEvent { id, event } => {
|
||||
backend.add_event(id, event)?;
|
||||
TicketBackendOperationResult::Unit
|
||||
|
|
@ -2218,6 +2249,79 @@ impl TicketBackend for LocalTicketBackend {
|
|||
})
|
||||
}
|
||||
|
||||
fn edit_item(&self, id: TicketIdOrSlug, edit: TicketItemEdit) -> Result<Ticket> {
|
||||
if edit.title.is_none() && edit.body.is_none() {
|
||||
return Err(TicketError::Conflict(
|
||||
"TicketEditItem requires at least one of title or body".to_string(),
|
||||
));
|
||||
}
|
||||
if let Some(title) = edit.title.as_deref() {
|
||||
validate_required_event_value("title", title)?;
|
||||
}
|
||||
if let Some(author) = edit.author.as_deref() {
|
||||
validate_required_event_value("author", author)?;
|
||||
}
|
||||
let _lock = self.acquire_lock()?;
|
||||
let dir = self.find_ticket_dir(&id)?;
|
||||
let item = dir.join("item.md");
|
||||
let mut content = fs::read_to_string(&item).map_err(|e| io_err(&item, e))?;
|
||||
let mut updates = Vec::new();
|
||||
if let Some(title) = edit.title.as_deref() {
|
||||
updates.push(("title", title));
|
||||
}
|
||||
if !updates.is_empty() {
|
||||
content = replace_frontmatter_fields(&content, &updates).map_err(|message| {
|
||||
TicketError::Parse {
|
||||
path: item.clone(),
|
||||
message,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
if let Some(body) = edit.body.as_ref() {
|
||||
content = replace_item_body(&content, body.as_str()).map_err(|message| {
|
||||
TicketError::Parse {
|
||||
path: item.clone(),
|
||||
message,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
atomic_write(&item, content.as_bytes())?;
|
||||
|
||||
let author = edit.author.unwrap_or_else(default_author);
|
||||
let mut changes = Vec::new();
|
||||
if edit.title.is_some() {
|
||||
changes.push("title");
|
||||
}
|
||||
if edit.body.is_some() {
|
||||
changes.push("body");
|
||||
}
|
||||
let body = MarkdownText::new(format!("Ticket item updated: {}.", changes.join(", ")));
|
||||
self.append_thread_event(
|
||||
&dir,
|
||||
"item_edit",
|
||||
self.generated_heading("Item updated", "項目更新"),
|
||||
&author,
|
||||
None,
|
||||
&[],
|
||||
&body,
|
||||
)?;
|
||||
self.ticket_from_dir(&dir)
|
||||
}
|
||||
|
||||
fn dependency_check(&self, id: TicketIdOrSlug) -> Result<TicketDependencyCheck> {
|
||||
let ticket = self.show(id)?;
|
||||
let summary = ticket_summary_from_meta(ticket.meta.clone());
|
||||
let projection = project_ticket_workspace_item(&summary, &ticket.relations.blockers, None);
|
||||
Ok(TicketDependencyCheck {
|
||||
ticket: summary,
|
||||
blockers: ticket.relations.blockers,
|
||||
queue_guard: projection.queue_guard,
|
||||
recommended_action: projection
|
||||
.next_action
|
||||
.unwrap_or(TicketWorkspaceNextAction::WaitForOrchestrator),
|
||||
})
|
||||
}
|
||||
|
||||
fn add_event(&self, id: TicketIdOrSlug, event: NewTicketEvent) -> Result<()> {
|
||||
let _lock = self.acquire_lock()?;
|
||||
let dir = self.find_ticket_dir(&id)?;
|
||||
|
|
@ -3737,6 +3841,32 @@ fn replace_frontmatter_fields(
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
fn replace_item_body(content: &str, body: &str) -> std::result::Result<String, String> {
|
||||
let mut lines = content.lines();
|
||||
if lines.next() != Some("---") {
|
||||
return Err("item.md missing frontmatter opener".to_string());
|
||||
}
|
||||
let mut frontmatter = vec!["---".to_string()];
|
||||
let mut found_close = false;
|
||||
for line in lines.by_ref() {
|
||||
frontmatter.push(line.to_string());
|
||||
if line == "---" {
|
||||
found_close = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_close {
|
||||
return Err("item.md missing frontmatter closer".to_string());
|
||||
}
|
||||
let mut out = frontmatter.join("\n");
|
||||
out.push_str("\n");
|
||||
out.push_str(body);
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn render_event_comment(attrs: &[(&str, &str)]) -> Result<String> {
|
||||
let mut out = String::from("<!--");
|
||||
for (key, value) in attrs {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use crate::{
|
|||
TicketDoctorSeverity, TicketError, TicketEventKind, TicketIdOrSlug, TicketIntakeSummary,
|
||||
TicketListState, TicketRef, TicketRelation, TicketRelationKind, TicketRelationView,
|
||||
TicketReview, TicketReviewResult, TicketStateChange, TicketSummary, TicketWorkflowState,
|
||||
default_author,
|
||||
};
|
||||
|
||||
const DEFAULT_LIST_LIMIT: usize = 50;
|
||||
|
|
@ -33,20 +34,27 @@ const MAX_BODY_MAX_BYTES: usize = 64 * 1024;
|
|||
const DEFAULT_DIAGNOSTIC_LIMIT: usize = 100;
|
||||
const MAX_DIAGNOSTIC_LIMIT: usize = 500;
|
||||
|
||||
pub const TICKET_BASE_TOOL_NAMES: [&str; 9] = [
|
||||
pub const TICKET_BASE_TOOL_NAMES: [&str; 12] = [
|
||||
"TicketCreate",
|
||||
"TicketEditItem",
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketQueue",
|
||||
"TicketWorkflowState",
|
||||
"TicketClose",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
pub const TICKET_BASE_READ_ONLY_TOOL_NAMES: [&str; 3] =
|
||||
["TicketList", "TicketShow", "TicketDoctor"];
|
||||
pub const TICKET_BASE_READ_ONLY_TOOL_NAMES: [&str; 4] = [
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
pub const TICKET_ORCHESTRATION_TOOL_NAMES: [&str; 4] = [
|
||||
"TicketRelationRecord",
|
||||
|
|
@ -58,35 +66,41 @@ pub const TICKET_ORCHESTRATION_TOOL_NAMES: [&str; 4] = [
|
|||
pub const TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES: [&str; 2] =
|
||||
["TicketRelationQuery", "TicketOrchestrationPlanQuery"];
|
||||
|
||||
pub const TICKET_TOOL_NAMES: [&str; 13] = [
|
||||
pub const TICKET_TOOL_NAMES: [&str; 16] = [
|
||||
"TicketCreate",
|
||||
"TicketEditItem",
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketQueue",
|
||||
"TicketWorkflowState",
|
||||
"TicketClose",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
"TicketRelationRecord",
|
||||
"TicketRelationQuery",
|
||||
"TicketOrchestrationPlanRecord",
|
||||
"TicketOrchestrationPlanQuery",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
pub const TICKET_READ_ONLY_TOOL_NAMES: [&str; 5] = [
|
||||
pub const TICKET_READ_ONLY_TOOL_NAMES: [&str; 6] = [
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
"TicketRelationQuery",
|
||||
"TicketOrchestrationPlanQuery",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
pub const TICKET_MUTATING_TOOL_NAMES: [&str; 8] = [
|
||||
pub const TICKET_MUTATING_TOOL_NAMES: [&str; 10] = [
|
||||
"TicketCreate",
|
||||
"TicketEditItem",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketQueue",
|
||||
"TicketWorkflowState",
|
||||
"TicketClose",
|
||||
"TicketRelationRecord",
|
||||
|
|
@ -96,6 +110,9 @@ pub const TICKET_MUTATING_TOOL_NAMES: [&str; 8] = [
|
|||
const CREATE_DESCRIPTION: &str = "Create a Ticket through the configured typed Ticket backend. \
|
||||
Inputs mirror the Ticket `item.md` fields; `title` is required, `body` is Markdown, and the \
|
||||
backend assigns the id and writes the local Ticket file layout under the configured backend root.";
|
||||
const EDIT_ITEM_DESCRIPTION: &str = "Edit a Ticket item through the configured typed Ticket backend. \
|
||||
This updates the current item title/body and appends an audited item_edit thread event. Intended for \
|
||||
User/Companion authoring surfaces, not Orchestrator implementation control.";
|
||||
const LIST_DESCRIPTION: &str = "List Tickets from the configured typed Ticket backend as a \
|
||||
lightweight bounded overview for selection only. Filter by query (`active`, `all`, a single workflow \
|
||||
state, or an explicit workflow-state list). Output is short summaries only; use TicketShow before \
|
||||
|
|
@ -111,6 +128,9 @@ const REVIEW_DESCRIPTION: &str = "Append a Ticket review event. `result` must be
|
|||
const INTAKE_READY_DESCRIPTION: &str = "Mark an existing Ticket planning lane ready through the typed \
|
||||
Ticket backend. The tool appends a bounded `intake_summary`, appends a typed `state_changed` event \
|
||||
for `state`, and transitions state to `ready`.";
|
||||
const QUEUE_DESCRIPTION: &str = "Queue a ready Ticket for Orchestrator routing through the typed \
|
||||
Ticket backend. The backend performs the gated ready -> queued transition, records queued_by/queued_at, \
|
||||
and rejects unresolved blocking relations.";
|
||||
const WORKFLOW_STATE_DESCRIPTION: &str = "Transition Ticket `state` through the typed \
|
||||
Ticket backend with a bounded `state_changed` event. Treat `queued -> inprogress` \
|
||||
as the implementation acceptance step: implementation side effects should happen only after that \
|
||||
|
|
@ -131,21 +151,26 @@ Ticket id and/or relation kind. This is read-only planning context; Orchestrator
|
|||
explicit state decisions.";
|
||||
const DOCTOR_DESCRIPTION: &str = "Run typed Ticket backend consistency checks and return bounded \
|
||||
diagnostics through the typed backend without shelling out to external commands.";
|
||||
const DEPENDENCY_CHECK_DESCRIPTION: &str = "Return a structured Ticket dependency / queue readiness \
|
||||
check through the typed Ticket backend. This read-only guard does not queue or transition the Ticket.";
|
||||
|
||||
fn base_tool_description(name: &str) -> &'static str {
|
||||
match name {
|
||||
"TicketCreate" => CREATE_DESCRIPTION,
|
||||
"TicketEditItem" => EDIT_ITEM_DESCRIPTION,
|
||||
"TicketList" => LIST_DESCRIPTION,
|
||||
"TicketShow" => SHOW_DESCRIPTION,
|
||||
"TicketComment" => COMMENT_DESCRIPTION,
|
||||
"TicketReview" => REVIEW_DESCRIPTION,
|
||||
"TicketIntakeReady" => INTAKE_READY_DESCRIPTION,
|
||||
"TicketQueue" => QUEUE_DESCRIPTION,
|
||||
"TicketWorkflowState" => WORKFLOW_STATE_DESCRIPTION,
|
||||
"TicketClose" => CLOSE_DESCRIPTION,
|
||||
"TicketRelationRecord" => RELATION_RECORD_DESCRIPTION,
|
||||
"TicketRelationQuery" => RELATION_QUERY_DESCRIPTION,
|
||||
"TicketOrchestrationPlanRecord" => ORCHESTRATION_PLAN_RECORD_DESCRIPTION,
|
||||
"TicketOrchestrationPlanQuery" => ORCHESTRATION_PLAN_QUERY_DESCRIPTION,
|
||||
"TicketDependencyCheck" => DEPENDENCY_CHECK_DESCRIPTION,
|
||||
"TicketDoctor" => DOCTOR_DESCRIPTION,
|
||||
_ => "Ticket backend tool.",
|
||||
}
|
||||
|
|
@ -226,6 +251,14 @@ impl TicketBackend for TicketToolBackend {
|
|||
self.backend.create(input)
|
||||
}
|
||||
|
||||
fn edit_item(&self, id: TicketIdOrSlug, edit: crate::TicketItemEdit) -> TicketResult<Ticket> {
|
||||
self.backend.edit_item(id, edit)
|
||||
}
|
||||
|
||||
fn dependency_check(&self, id: TicketIdOrSlug) -> TicketResult<crate::TicketDependencyCheck> {
|
||||
self.backend.dependency_check(id)
|
||||
}
|
||||
|
||||
fn add_event(&self, id: TicketIdOrSlug, event: NewTicketEvent) -> TicketResult<()> {
|
||||
self.backend.add_event(id, event)
|
||||
}
|
||||
|
|
@ -351,6 +384,21 @@ struct TicketCreateParams {
|
|||
queued_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketEditItemParams {
|
||||
/// Ticket id.
|
||||
ticket: String,
|
||||
/// Optional replacement title.
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
/// Optional replacement Markdown body.
|
||||
#[serde(default)]
|
||||
body: Option<String>,
|
||||
/// Optional thread author for the audited item_edit event.
|
||||
#[serde(default)]
|
||||
author: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum TicketWorkflowStateParam {
|
||||
|
|
@ -534,6 +582,15 @@ struct TicketIntakeReadyParams {
|
|||
state_change_body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketQueueParams {
|
||||
/// Ticket id.
|
||||
ticket: String,
|
||||
/// Optional queued_by frontmatter value. Defaults to the backend/user default.
|
||||
#[serde(default)]
|
||||
queued_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketWorkflowStateParams {
|
||||
/// Ticket id.
|
||||
|
|
@ -559,6 +616,12 @@ struct TicketCloseParams {
|
|||
resolution: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
struct TicketDependencyCheckParams {
|
||||
/// Ticket id.
|
||||
ticket: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum TicketRelationKindParam {
|
||||
|
|
@ -750,6 +813,11 @@ struct TicketCreateTool {
|
|||
backend: TicketToolBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketEditItemTool {
|
||||
backend: TicketToolBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketListTool {
|
||||
backend: TicketToolBackend,
|
||||
|
|
@ -775,6 +843,11 @@ struct TicketIntakeReadyTool {
|
|||
backend: TicketToolBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketQueueTool {
|
||||
backend: TicketToolBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketWorkflowStateTool {
|
||||
backend: TicketToolBackend,
|
||||
|
|
@ -810,6 +883,11 @@ struct TicketDoctorTool {
|
|||
backend: TicketToolBackend,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TicketDependencyCheckTool {
|
||||
backend: TicketToolBackend,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketCreateTool {
|
||||
async fn execute(
|
||||
|
|
@ -844,6 +922,35 @@ impl Tool for TicketCreateTool {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketEditItemTool {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: TicketEditItemParams = parse_input("TicketEditItem", input_json)?;
|
||||
let edit = crate::TicketItemEdit {
|
||||
title: params.title,
|
||||
body: params.body.map(MarkdownText::new),
|
||||
author: params.author,
|
||||
};
|
||||
let ticket = self
|
||||
.backend
|
||||
.edit_item(TicketIdOrSlug::from(params.ticket), edit)
|
||||
.map_err(|error| backend_error("TicketEditItem", error))?;
|
||||
Ok(json_output(
|
||||
format!("Edited ticket {}", ticket.meta.id),
|
||||
ticket_json(
|
||||
&ticket,
|
||||
DEFAULT_EVENT_LIMIT,
|
||||
DEFAULT_ARTIFACT_LIMIT,
|
||||
16 * 1024,
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketListTool {
|
||||
async fn execute(
|
||||
|
|
@ -1015,6 +1122,25 @@ impl Tool for TicketIntakeReadyTool {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketQueueTool {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: TicketQueueParams = parse_input("TicketQueue", input_json)?;
|
||||
let queued_by = params.queued_by.unwrap_or_else(default_author);
|
||||
self.backend
|
||||
.queue_ready(TicketIdOrSlug::Query(params.ticket.clone()), &queued_by)
|
||||
.map_err(|error| backend_error("TicketQueue", error))?;
|
||||
Ok(json_output(
|
||||
format!("Queued ticket {} for Orchestrator", params.ticket),
|
||||
json!({ "ticket": params.ticket, "state": "queued", "queued_by": queued_by, "ok": true }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketWorkflowStateTool {
|
||||
async fn execute(
|
||||
|
|
@ -1244,6 +1370,33 @@ impl Tool for TicketDoctorTool {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for TicketDependencyCheckTool {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_engine::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: TicketDependencyCheckParams = parse_input("TicketDependencyCheck", input_json)?;
|
||||
let check = self
|
||||
.backend
|
||||
.dependency_check(TicketIdOrSlug::Query(params.ticket.clone()))
|
||||
.map_err(|error| backend_error("TicketDependencyCheck", error))?;
|
||||
Ok(json_output(
|
||||
format!(
|
||||
"Ticket {} dependency check: {}",
|
||||
params.ticket,
|
||||
if check.queue_guard.can_queue_for_orchestrator {
|
||||
"queueable"
|
||||
} else {
|
||||
"not queueable"
|
||||
}
|
||||
),
|
||||
check,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_input<T: for<'de> Deserialize<'de>>(tool: &str, input_json: &str) -> Result<T, ToolError> {
|
||||
serde_json::from_str(input_json)
|
||||
.map_err(|error| ToolError::InvalidArgument(format!("invalid {tool} input: {error}")))
|
||||
|
|
@ -1509,15 +1662,20 @@ where
|
|||
fn input_schema(name: &str) -> Value {
|
||||
match name {
|
||||
"TicketCreate" => serde_json::to_value(schemars::schema_for!(TicketCreateParams)),
|
||||
"TicketEditItem" => serde_json::to_value(schemars::schema_for!(TicketEditItemParams)),
|
||||
"TicketList" => serde_json::to_value(schemars::schema_for!(TicketListParams)),
|
||||
"TicketShow" => serde_json::to_value(schemars::schema_for!(TicketShowParams)),
|
||||
"TicketComment" => serde_json::to_value(schemars::schema_for!(TicketCommentParams)),
|
||||
"TicketReview" => serde_json::to_value(schemars::schema_for!(TicketReviewParams)),
|
||||
"TicketIntakeReady" => serde_json::to_value(schemars::schema_for!(TicketIntakeReadyParams)),
|
||||
"TicketQueue" => serde_json::to_value(schemars::schema_for!(TicketQueueParams)),
|
||||
"TicketWorkflowState" => {
|
||||
serde_json::to_value(schemars::schema_for!(TicketWorkflowStateParams))
|
||||
}
|
||||
"TicketClose" => serde_json::to_value(schemars::schema_for!(TicketCloseParams)),
|
||||
"TicketDependencyCheck" => {
|
||||
serde_json::to_value(schemars::schema_for!(TicketDependencyCheckParams))
|
||||
}
|
||||
"TicketRelationRecord" => {
|
||||
serde_json::to_value(schemars::schema_for!(TicketRelationRecordParams))
|
||||
}
|
||||
|
|
@ -1547,11 +1705,13 @@ macro_rules! impl_from_backend {
|
|||
}
|
||||
|
||||
impl_from_backend!(TicketCreateTool);
|
||||
impl_from_backend!(TicketEditItemTool);
|
||||
impl_from_backend!(TicketListTool);
|
||||
impl_from_backend!(TicketShowTool);
|
||||
impl_from_backend!(TicketCommentTool);
|
||||
impl_from_backend!(TicketReviewTool);
|
||||
impl_from_backend!(TicketIntakeReadyTool);
|
||||
impl_from_backend!(TicketQueueTool);
|
||||
impl_from_backend!(TicketWorkflowStateTool);
|
||||
impl_from_backend!(TicketCloseTool);
|
||||
impl_from_backend!(TicketRelationRecordTool);
|
||||
|
|
@ -1559,19 +1719,24 @@ impl_from_backend!(TicketRelationQueryTool);
|
|||
impl_from_backend!(TicketOrchestrationPlanRecordTool);
|
||||
impl_from_backend!(TicketOrchestrationPlanQueryTool);
|
||||
impl_from_backend!(TicketDoctorTool);
|
||||
impl_from_backend!(TicketDependencyCheckTool);
|
||||
|
||||
/// Build all MVP Ticket tool definitions over the supplied backend.
|
||||
pub fn ticket_tools(backend: impl Into<TicketToolBackend>) -> Vec<ToolDefinition> {
|
||||
let backend = backend.into();
|
||||
vec![
|
||||
tool_definition::<TicketCreateTool>("TicketCreate", backend.clone()),
|
||||
tool_definition::<TicketEditItemTool>("TicketEditItem", backend.clone()),
|
||||
tool_definition::<TicketListTool>("TicketList", backend.clone()),
|
||||
tool_definition::<TicketShowTool>("TicketShow", backend.clone()),
|
||||
tool_definition::<TicketCommentTool>("TicketComment", backend.clone()),
|
||||
tool_definition::<TicketReviewTool>("TicketReview", backend.clone()),
|
||||
tool_definition::<TicketIntakeReadyTool>("TicketIntakeReady", backend.clone()),
|
||||
tool_definition::<TicketQueueTool>("TicketQueue", backend.clone()),
|
||||
tool_definition::<TicketWorkflowStateTool>("TicketWorkflowState", backend.clone()),
|
||||
tool_definition::<TicketCloseTool>("TicketClose", backend.clone()),
|
||||
tool_definition::<TicketDependencyCheckTool>("TicketDependencyCheck", backend.clone()),
|
||||
tool_definition::<TicketDoctorTool>("TicketDoctor", backend.clone()),
|
||||
tool_definition::<TicketRelationRecordTool>("TicketRelationRecord", backend.clone()),
|
||||
tool_definition::<TicketRelationQueryTool>("TicketRelationQuery", backend.clone()),
|
||||
tool_definition::<TicketOrchestrationPlanRecordTool>(
|
||||
|
|
@ -1582,7 +1747,6 @@ pub fn ticket_tools(backend: impl Into<TicketToolBackend>) -> Vec<ToolDefinition
|
|||
"TicketOrchestrationPlanQuery",
|
||||
backend.clone(),
|
||||
),
|
||||
tool_definition::<TicketDoctorTool>("TicketDoctor", backend),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1627,18 +1791,21 @@ mod tests {
|
|||
[
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
"TicketRelationQuery",
|
||||
"TicketOrchestrationPlanQuery",
|
||||
"TicketDoctor"
|
||||
"TicketOrchestrationPlanQuery"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
TICKET_MUTATING_TOOL_NAMES,
|
||||
[
|
||||
"TicketCreate",
|
||||
"TicketEditItem",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketIntakeReady",
|
||||
"TicketQueue",
|
||||
"TicketWorkflowState",
|
||||
"TicketClose",
|
||||
"TicketRelationRecord",
|
||||
|
|
|
|||
|
|
@ -548,7 +548,10 @@ fn install_ticket_event_companion_notify_hook<C, St>(
|
|||
|
||||
let ticket_feature = &worker.manifest().feature.ticket;
|
||||
if !ticket_feature.enabled
|
||||
|| !matches!(ticket_feature.access, TicketFeatureAccessConfig::Lifecycle)
|
||||
|| !matches!(
|
||||
ticket_feature.access,
|
||||
TicketFeatureAccessConfig::OrchestrationControl | TicketFeatureAccessConfig::Lifecycle
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -679,6 +682,21 @@ where
|
|||
TicketFeatureAccessConfig::ReadOnly => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly
|
||||
}
|
||||
TicketFeatureAccessConfig::WorkspaceAuthoring => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::WorkspaceAuthoring
|
||||
}
|
||||
TicketFeatureAccessConfig::Intake => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::Intake
|
||||
}
|
||||
TicketFeatureAccessConfig::OrchestrationControl => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::OrchestrationControl
|
||||
}
|
||||
TicketFeatureAccessConfig::WorkReport => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::WorkReport
|
||||
}
|
||||
TicketFeatureAccessConfig::Review => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::Review
|
||||
}
|
||||
TicketFeatureAccessConfig::Lifecycle => {
|
||||
crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ use ticket::{
|
|||
tool::{
|
||||
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
|
||||
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||
TICKET_READ_ONLY_TOOL_NAMES, TICKET_TOOL_NAMES, TicketToolBackend, ticket_tool_description,
|
||||
ticket_tools,
|
||||
TicketToolBackend, ticket_tool_description, ticket_tools,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -34,24 +33,96 @@ The tools operate through the ticket crate backend and do not grant generic file
|
|||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TicketFeatureAccess {
|
||||
/// Status/diagnostic access for views such as Companion that must not mutate Tickets.
|
||||
/// Status/diagnostic access for views that must not mutate Tickets.
|
||||
ReadOnly,
|
||||
/// Full Ticket lifecycle access, including the read-only tools and all mutating Ticket tools.
|
||||
/// User/Companion authoring access for shaping current Ticket specs and queueing work.
|
||||
WorkspaceAuthoring,
|
||||
/// Intake access for creating/refining planning Tickets and marking them ready.
|
||||
Intake,
|
||||
/// Orchestrator control access for state/thread/relation/orchestration operations.
|
||||
OrchestrationControl,
|
||||
/// Coder-style access for reading Tickets and writing implementation reports/comments.
|
||||
WorkReport,
|
||||
/// Reviewer-style access for reading Tickets and writing review/comment thread events.
|
||||
Review,
|
||||
/// Legacy full lifecycle access retained as a migration shim.
|
||||
Lifecycle,
|
||||
}
|
||||
|
||||
const WORKSPACE_AUTHORING_BASE_TOOL_NAMES: [&str; 10] = [
|
||||
"TicketCreate",
|
||||
"TicketEditItem",
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketQueue",
|
||||
"TicketClose",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
const INTAKE_BASE_TOOL_NAMES: [&str; 8] = [
|
||||
"TicketCreate",
|
||||
"TicketEditItem",
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketIntakeReady",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
const ORCHESTRATION_CONTROL_BASE_TOOL_NAMES: [&str; 8] = [
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketWorkflowState",
|
||||
"TicketClose",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
const WORK_REPORT_BASE_TOOL_NAMES: [&str; 5] = [
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
const REVIEW_BASE_TOOL_NAMES: [&str; 6] = [
|
||||
"TicketList",
|
||||
"TicketShow",
|
||||
"TicketComment",
|
||||
"TicketReview",
|
||||
"TicketDependencyCheck",
|
||||
"TicketDoctor",
|
||||
];
|
||||
|
||||
const RELATION_WRITE_TOOL_NAMES: [&str; 2] = ["TicketRelationRecord", "TicketRelationQuery"];
|
||||
|
||||
impl TicketFeatureAccess {
|
||||
pub fn base_tool_names(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::ReadOnly => &TICKET_BASE_READ_ONLY_TOOL_NAMES,
|
||||
Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_BASE_TOOL_NAMES,
|
||||
Self::Intake => &INTAKE_BASE_TOOL_NAMES,
|
||||
Self::OrchestrationControl => &ORCHESTRATION_CONTROL_BASE_TOOL_NAMES,
|
||||
Self::WorkReport => &WORK_REPORT_BASE_TOOL_NAMES,
|
||||
Self::Review => &REVIEW_BASE_TOOL_NAMES,
|
||||
Self::Lifecycle => &TICKET_BASE_TOOL_NAMES,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn orchestration_tool_names(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::ReadOnly => &TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES,
|
||||
Self::Lifecycle => &TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||
Self::ReadOnly | Self::WorkReport | Self::Review => {
|
||||
&TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES
|
||||
}
|
||||
Self::WorkspaceAuthoring | Self::Intake => &RELATION_WRITE_TOOL_NAMES,
|
||||
Self::OrchestrationControl | Self::Lifecycle => &TICKET_ORCHESTRATION_TOOL_NAMES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,12 +271,6 @@ impl TicketFeature {
|
|||
}
|
||||
|
||||
fn enabled_tool_names(&self) -> Vec<&'static str> {
|
||||
if self.include_base_tools && self.include_orchestration_tools {
|
||||
return match self.access {
|
||||
TicketFeatureAccess::ReadOnly => TICKET_READ_ONLY_TOOL_NAMES.to_vec(),
|
||||
TicketFeatureAccess::Lifecycle => TICKET_TOOL_NAMES.to_vec(),
|
||||
};
|
||||
}
|
||||
let mut names = Vec::new();
|
||||
if self.include_base_tools {
|
||||
names.extend_from_slice(self.access.base_tool_names());
|
||||
|
|
@ -421,6 +486,20 @@ impl TicketBackend for WorkspaceHttpTicketBackend {
|
|||
)
|
||||
}
|
||||
|
||||
fn edit_item(&self, id: TicketIdOrSlug, edit: ticket::TicketItemEdit) -> TicketResult<Ticket> {
|
||||
expect_ticket_result!(
|
||||
self.invoke(TicketBackendOperation::EditItem { id, edit }),
|
||||
TicketBackendOperationResult::Ticket
|
||||
)
|
||||
}
|
||||
|
||||
fn dependency_check(&self, id: TicketIdOrSlug) -> TicketResult<ticket::TicketDependencyCheck> {
|
||||
expect_ticket_result!(
|
||||
self.invoke(TicketBackendOperation::DependencyCheck { id }),
|
||||
TicketBackendOperationResult::DependencyCheck
|
||||
)
|
||||
}
|
||||
|
||||
fn add_event(&self, id: TicketIdOrSlug, event: NewTicketEvent) -> TicketResult<()> {
|
||||
match self.invoke(TicketBackendOperation::AddEvent { id, event })? {
|
||||
TicketBackendOperationResult::Unit => Ok(()),
|
||||
|
|
@ -715,6 +794,74 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn semantic_access_presets_expose_role_capability_surfaces() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
||||
let workspace_authoring = ticket_tools_feature_with_options(
|
||||
temp.path(),
|
||||
Some(TicketFeatureAccess::WorkspaceAuthoring),
|
||||
false,
|
||||
);
|
||||
let workspace_descriptor = workspace_authoring.descriptor();
|
||||
let workspace_tools = workspace_descriptor
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(workspace_tools.contains(&"TicketCreate"));
|
||||
assert!(workspace_tools.contains(&"TicketEditItem"));
|
||||
assert!(workspace_tools.contains(&"TicketQueue"));
|
||||
assert!(!workspace_tools.contains(&"TicketWorkflowState"));
|
||||
|
||||
let orchestration = ticket_tools_feature_with_options(
|
||||
temp.path(),
|
||||
Some(TicketFeatureAccess::OrchestrationControl),
|
||||
true,
|
||||
);
|
||||
let orchestration_descriptor = orchestration.descriptor();
|
||||
let orchestration_tools = orchestration_descriptor
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(orchestration_tools.contains(&"TicketWorkflowState"));
|
||||
assert!(orchestration_tools.contains(&"TicketDependencyCheck"));
|
||||
assert!(orchestration_tools.contains(&"TicketRelationRecord"));
|
||||
assert!(orchestration_tools.contains(&"TicketOrchestrationPlanRecord"));
|
||||
assert!(!orchestration_tools.contains(&"TicketEditItem"));
|
||||
assert!(!orchestration_tools.contains(&"TicketQueue"));
|
||||
|
||||
let work_report = ticket_tools_feature_with_options(
|
||||
temp.path(),
|
||||
Some(TicketFeatureAccess::WorkReport),
|
||||
false,
|
||||
);
|
||||
let work_report_descriptor = work_report.descriptor();
|
||||
let work_report_tools = work_report_descriptor
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(work_report_tools.contains(&"TicketComment"));
|
||||
assert!(!work_report_tools.contains(&"TicketReview"));
|
||||
assert!(!work_report_tools.contains(&"TicketWorkflowState"));
|
||||
|
||||
let review = ticket_tools_feature_with_options(
|
||||
temp.path(),
|
||||
Some(TicketFeatureAccess::Review),
|
||||
false,
|
||||
);
|
||||
let review_descriptor = review.descriptor();
|
||||
let review_tools = review_descriptor
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(review_tools.contains(&"TicketReview"));
|
||||
assert!(!review_tools.contains(&"TicketWorkflowState"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_only_installation_does_not_expose_mutating_tools() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user