ticket: collapse ticket access into one feature

This commit is contained in:
Keisuke Hirata 2026-07-20 19:51:46 +09:00
parent 2c01e7672b
commit 064965d350
No known key found for this signature in database
13 changed files with 147 additions and 280 deletions

View File

@ -87,8 +87,6 @@ pub struct FeatureConfigPartial {
#[serde(default)] #[serde(default)]
pub ticket: Option<TicketFeatureConfigPartial>, pub ticket: Option<TicketFeatureConfigPartial>,
#[serde(default)] #[serde(default)]
pub ticket_orchestration: Option<FeatureFlagConfigPartial>,
#[serde(default)]
pub plugins: Option<FeatureFlagConfigPartial>, pub plugins: Option<FeatureFlagConfigPartial>,
} }
@ -100,11 +98,6 @@ impl FeatureConfigPartial {
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge), web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::merge), workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::merge),
ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge), ticket: merge_option(self.ticket, other.ticket, TicketFeatureConfigPartial::merge),
ticket_orchestration: merge_option(
self.ticket_orchestration,
other.ticket_orchestration,
FeatureFlagConfigPartial::merge,
),
plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge), plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge),
} }
} }
@ -128,10 +121,6 @@ impl FeatureFlagConfigPartial {
pub struct TicketFeatureConfigPartial { pub struct TicketFeatureConfigPartial {
#[serde(default)] #[serde(default)]
pub enabled: Option<bool>, 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)] #[serde(default)]
pub preset: Option<TicketFeatureAccessConfig>, pub preset: Option<TicketFeatureAccessConfig>,
} }
@ -140,7 +129,6 @@ impl TicketFeatureConfigPartial {
fn merge(self, other: Self) -> Self { fn merge(self, other: Self) -> Self {
Self { Self {
enabled: other.enabled.or(self.enabled), enabled: other.enabled.or(self.enabled),
access: other.access.or(self.access),
preset: other.preset.or(self.preset), preset: other.preset.or(self.preset),
} }
} }
@ -163,10 +151,6 @@ impl From<FeatureConfigPartial> for FeatureConfig {
.ticket .ticket
.map(TicketFeatureConfig::from) .map(TicketFeatureConfig::from)
.unwrap_or_default(), .unwrap_or_default(),
ticket_orchestration: value
.ticket_orchestration
.map(FeatureFlagConfig::from)
.unwrap_or_default(),
plugins: value plugins: value
.plugins .plugins
.map(FeatureFlagConfig::from) .map(FeatureFlagConfig::from)
@ -195,7 +179,7 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
fn from(value: TicketFeatureConfigPartial) -> Self { fn from(value: TicketFeatureConfigPartial) -> Self {
Self { Self {
enabled: value.enabled.unwrap_or_default(), enabled: value.enabled.unwrap_or_default(),
access: value.preset.or(value.access).unwrap_or_default(), preset: value.preset.unwrap_or_default(),
} }
} }
} }
@ -204,8 +188,7 @@ impl From<TicketFeatureConfig> for TicketFeatureConfigPartial {
fn from(value: TicketFeatureConfig) -> Self { fn from(value: TicketFeatureConfig) -> Self {
Self { Self {
enabled: Some(value.enabled), enabled: Some(value.enabled),
access: Some(value.access), preset: Some(value.preset),
preset: None,
} }
} }
} }
@ -218,7 +201,6 @@ impl From<FeatureConfig> for FeatureConfigPartial {
web: Some(value.web.into()), web: Some(value.web.into()),
workers: Some(value.workers.into()), workers: Some(value.workers.into()),
ticket: Some(value.ticket.into()), ticket: Some(value.ticket.into()),
ticket_orchestration: Some(value.ticket_orchestration.into()),
plugins: Some(value.plugins.into()), plugins: Some(value.plugins.into()),
} }
} }
@ -1764,7 +1746,6 @@ worker_max_turns = 7
assert!(!manifest.feature.web.enabled); assert!(!manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled); assert!(!manifest.feature.workers.enabled);
assert!(!manifest.feature.ticket.enabled); assert!(!manifest.feature.ticket.enabled);
assert!(!manifest.feature.ticket_orchestration.enabled);
} }
#[test] #[test]
@ -1776,10 +1757,7 @@ enabled = true
[feature.ticket] [feature.ticket]
enabled = true enabled = true
access = "read_only" preset = "read_only"
[feature.ticket_orchestration]
enabled = true
"#, "#,
) )
.unwrap(); .unwrap();
@ -1810,10 +1788,9 @@ enabled = true
assert!(manifest.feature.task.enabled); assert!(manifest.feature.task.enabled);
assert!(manifest.feature.ticket.enabled); assert!(manifest.feature.ticket.enabled);
assert_eq!( assert_eq!(
manifest.feature.ticket.access, manifest.feature.ticket.preset,
TicketFeatureAccessConfig::ReadOnly TicketFeatureAccessConfig::ReadOnly
); );
assert!(manifest.feature.ticket_orchestration.enabled);
assert!(!manifest.feature.memory.enabled); assert!(!manifest.feature.memory.enabled);
} }
@ -1826,14 +1803,14 @@ enabled = true
[feature.ticket] [feature.ticket]
enabled = true enabled = true
access = "read_only" preset = "read_only"
"#, "#,
) )
.unwrap(); .unwrap();
let upper = WorkerManifestConfig::from_toml( let upper = WorkerManifestConfig::from_toml(
r#" r#"
[feature.ticket] [feature.ticket]
access = "lifecycle" preset = "orchestration_control"
[feature.web] [feature.web]
enabled = true enabled = true
@ -1868,8 +1845,8 @@ enabled = true
assert!(manifest.feature.memory.enabled); assert!(manifest.feature.memory.enabled);
assert!(manifest.feature.ticket.enabled); assert!(manifest.feature.ticket.enabled);
assert_eq!( assert_eq!(
manifest.feature.ticket.access, manifest.feature.ticket.preset,
TicketFeatureAccessConfig::Lifecycle TicketFeatureAccessConfig::OrchestrationControl
); );
assert!(manifest.feature.web.enabled); assert!(manifest.feature.web.enabled);
assert!(!manifest.feature.workers.enabled); assert!(!manifest.feature.workers.enabled);

View File

@ -115,8 +115,6 @@ pub struct FeatureConfig {
#[serde(default)] #[serde(default)]
pub ticket: TicketFeatureConfig, pub ticket: TicketFeatureConfig,
#[serde(default)] #[serde(default)]
pub ticket_orchestration: FeatureFlagConfig,
#[serde(default)]
pub plugins: FeatureFlagConfig, pub plugins: FeatureFlagConfig,
} }
@ -128,7 +126,6 @@ impl Default for FeatureConfig {
web: FeatureFlagConfig::disabled(), web: FeatureFlagConfig::disabled(),
workers: FeatureFlagConfig::disabled(), workers: FeatureFlagConfig::disabled(),
ticket: TicketFeatureConfig::default(), ticket: TicketFeatureConfig::default(),
ticket_orchestration: FeatureFlagConfig::disabled(),
plugins: FeatureFlagConfig::disabled(), plugins: FeatureFlagConfig::disabled(),
} }
} }
@ -160,18 +157,15 @@ impl Default for FeatureFlagConfig {
pub struct TicketFeatureConfig { pub struct TicketFeatureConfig {
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,
/// Which non-orchestration Ticket surface to expose when `enabled = true`.
/// Orchestration-plan/relation tools are controlled independently by
/// `[feature.ticket_orchestration].enabled`.
#[serde(default)] #[serde(default)]
pub access: TicketFeatureAccessConfig, pub preset: TicketFeatureAccessConfig,
} }
impl Default for TicketFeatureConfig { impl Default for TicketFeatureConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: false, enabled: false,
access: TicketFeatureAccessConfig::Lifecycle, preset: TicketFeatureAccessConfig::default(),
} }
} }
} }
@ -185,13 +179,11 @@ pub enum TicketFeatureAccessConfig {
OrchestrationControl, OrchestrationControl,
WorkReport, WorkReport,
Review, Review,
/// Legacy broad mutation preset retained as a migration shim.
Lifecycle,
} }
impl Default for TicketFeatureAccessConfig { impl Default for TicketFeatureAccessConfig {
fn default() -> Self { fn default() -> Self {
Self::Lifecycle Self::WorkspaceAuthoring
} }
} }

View File

@ -894,7 +894,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true, true,
true, true,
true, true,
false,
); );
Some(value) Some(value)
} }
@ -908,7 +907,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true, true,
true, true,
false, false,
false,
); );
Some(value) Some(value)
} }
@ -922,7 +920,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true, true,
true, true,
true, true,
true,
); );
Some(value) Some(value)
} }
@ -936,7 +933,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true, true,
true, true,
false, false,
false,
); );
Some(value) Some(value)
} }
@ -950,7 +946,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
true, true,
true, true,
false, false,
false,
); );
Some(value) Some(value)
} }
@ -976,8 +971,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value {
"memory": { "enabled": true }, "memory": { "enabled": true },
"web": { "enabled": true }, "web": { "enabled": true },
"workers": { "enabled": true }, "workers": { "enabled": true },
"ticket": { "enabled": true, "access": "lifecycle" }, "ticket": { "enabled": true, "preset": "workspace_authoring" }
"ticket_orchestration": { "enabled": false }
}, },
"memory": { "memory": {
"extract_threshold": 50000, "extract_threshold": 50000,
@ -1004,7 +998,6 @@ fn apply_role_profile(
memory: bool, memory: bool,
web: bool, web: bool,
workers: bool, workers: bool,
ticket_orchestration: bool,
) { ) {
value["slug"] = serde_json::Value::String(slug.to_string()); value["slug"] = serde_json::Value::String(slug.to_string());
value["description"] = serde_json::Value::String(description.to_string()); value["description"] = serde_json::Value::String(description.to_string());
@ -1018,14 +1011,12 @@ fn apply_role_profile(
"orchestrator" => TicketFeatureAccessConfig::OrchestrationControl, "orchestrator" => TicketFeatureAccessConfig::OrchestrationControl,
"coder" => TicketFeatureAccessConfig::WorkReport, "coder" => TicketFeatureAccessConfig::WorkReport,
"reviewer" => TicketFeatureAccessConfig::Review, "reviewer" => TicketFeatureAccessConfig::Review,
_ => TicketFeatureAccessConfig::Lifecycle, _ => TicketFeatureAccessConfig::WorkspaceAuthoring,
}; };
value["feature"]["ticket"] = serde_json::json!({ value["feature"]["ticket"] = serde_json::json!({
"enabled": true, "enabled": true,
"preset": ticket_access, "preset": ticket_access,
}); });
value["feature"]["ticket_orchestration"] =
serde_json::json!({ "enabled": ticket_orchestration });
} }
fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> { fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> {
@ -1452,10 +1443,9 @@ mod tests {
assert!(companion.web.is_some()); assert!(companion.web.is_some());
assert!(companion.feature.ticket.enabled); assert!(companion.feature.ticket.enabled);
assert_eq!( assert_eq!(
companion.feature.ticket.access, companion.feature.ticket.preset,
TicketFeatureAccessConfig::WorkspaceAuthoring TicketFeatureAccessConfig::WorkspaceAuthoring
); );
assert!(!companion.feature.ticket_orchestration.enabled);
assert_eq!( assert_eq!(
companion.compaction.as_ref().unwrap().threshold, companion.compaction.as_ref().unwrap().threshold,
Some(240000) Some(240000)
@ -1478,7 +1468,7 @@ mod tests {
assert!(!intake.feature.workers.enabled); assert!(!intake.feature.workers.enabled);
assert!(intake.feature.ticket.enabled); assert!(intake.feature.ticket.enabled);
assert_eq!( assert_eq!(
intake.feature.ticket.access, intake.feature.ticket.preset,
TicketFeatureAccessConfig::Intake TicketFeatureAccessConfig::Intake
); );
assert!(intake.scope.allow.is_empty()); assert!(intake.scope.allow.is_empty());
@ -1486,17 +1476,15 @@ mod tests {
assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert_eq!(intake.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
assert!(intake.web.is_some()); assert!(intake.web.is_some());
assert!(intake.compaction.is_some()); assert!(intake.compaction.is_some());
assert!(!intake.feature.ticket_orchestration.enabled);
let orchestrator = resolve("orchestrator"); let orchestrator = resolve("orchestrator");
assert!(orchestrator.feature.task.enabled); assert!(orchestrator.feature.task.enabled);
assert!(orchestrator.feature.workers.enabled); assert!(orchestrator.feature.workers.enabled);
assert!(orchestrator.feature.ticket.enabled); assert!(orchestrator.feature.ticket.enabled);
assert_eq!( assert_eq!(
orchestrator.feature.ticket.access, orchestrator.feature.ticket.preset,
TicketFeatureAccessConfig::OrchestrationControl TicketFeatureAccessConfig::OrchestrationControl
); );
assert!(orchestrator.feature.ticket_orchestration.enabled);
assert!(orchestrator.scope.allow.is_empty()); assert!(orchestrator.scope.allow.is_empty());
assert!(orchestrator.delegation_scope.allow.is_empty()); assert!(orchestrator.delegation_scope.allow.is_empty());
assert_eq!( assert_eq!(
@ -1516,20 +1504,18 @@ mod tests {
assert!(coder.compaction.is_some()); assert!(coder.compaction.is_some());
assert!(coder.feature.ticket.enabled); assert!(coder.feature.ticket.enabled);
assert_eq!( assert_eq!(
coder.feature.ticket.access, coder.feature.ticket.preset,
TicketFeatureAccessConfig::WorkReport TicketFeatureAccessConfig::WorkReport
); );
assert!(!coder.feature.ticket_orchestration.enabled);
let reviewer = resolve("reviewer"); let reviewer = resolve("reviewer");
assert!(reviewer.feature.task.enabled); assert!(reviewer.feature.task.enabled);
assert!(!reviewer.feature.workers.enabled); assert!(!reviewer.feature.workers.enabled);
assert!(reviewer.feature.ticket.enabled); assert!(reviewer.feature.ticket.enabled);
assert_eq!( assert_eq!(
reviewer.feature.ticket.access, reviewer.feature.ticket.preset,
TicketFeatureAccessConfig::Review TicketFeatureAccessConfig::Review
); );
assert!(!reviewer.feature.ticket_orchestration.enabled);
assert!(reviewer.scope.allow.is_empty()); assert!(reviewer.scope.allow.is_empty());
assert!(reviewer.delegation_scope.allow.is_empty()); assert!(reviewer.delegation_scope.allow.is_empty());
assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5")); assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
@ -1678,10 +1664,7 @@ enabled = true
[feature.ticket] [feature.ticket]
enabled = true enabled = true
access = "read_only" preset = "read_only"
[feature.ticket_orchestration]
enabled = false
"#, "#,
); );
let workspace = tmp.path().join("workspace"); let workspace = tmp.path().join("workspace");
@ -1700,10 +1683,9 @@ enabled = false
assert!(resolved.manifest.feature.workers.enabled); assert!(resolved.manifest.feature.workers.enabled);
assert!(resolved.manifest.feature.ticket.enabled); assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!( assert_eq!(
resolved.manifest.feature.ticket.access, resolved.manifest.feature.ticket.preset,
crate::TicketFeatureAccessConfig::ReadOnly crate::TicketFeatureAccessConfig::ReadOnly
); );
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
assert_eq!( assert_eq!(
resolved.manifest.delegation_scope.allow[0].target, resolved.manifest.delegation_scope.allow[0].target,
workspace workspace
@ -1791,10 +1773,9 @@ worker_context_max_tokens = 68000
assert!(resolved.manifest.session.record_event_trace); assert!(resolved.manifest.session.record_event_trace);
assert!(resolved.manifest.feature.ticket.enabled); assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!( assert_eq!(
resolved.manifest.feature.ticket.access, resolved.manifest.feature.ticket.preset,
crate::TicketFeatureAccessConfig::Lifecycle crate::TicketFeatureAccessConfig::WorkspaceAuthoring
); );
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
assert_eq!( assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(), resolved.profile.as_ref().unwrap().name.as_deref(),
Some("default") Some("default")

View File

@ -238,10 +238,7 @@ enabled = false
[feature.ticket] [feature.ticket]
enabled = true enabled = true
access = "lifecycle" preset = "workspace_authoring"
[feature.ticket_orchestration]
enabled = false
[memory] [memory]
extract_threshold = 50000 extract_threshold = 50000
@ -298,8 +295,9 @@ mod tests {
assert!(profile.contains("slug = \"default\"")); assert!(profile.contains("slug = \"default\""));
assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\"")); assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\""));
assert!(profile.contains("scope = \"workspace_write\"")); assert!(profile.contains("scope = \"workspace_write\""));
assert!(profile.contains("[feature.ticket]\nenabled = true\naccess = \"lifecycle\"")); assert!(
assert!(profile.contains("[feature.ticket_orchestration]\nenabled = false")); profile.contains("[feature.ticket]\nenabled = true\npreset = \"workspace_authoring\"")
);
} }
#[test] #[test]

View File

@ -548,10 +548,7 @@ fn install_ticket_event_companion_notify_hook<C, St>(
let ticket_feature = &worker.manifest().feature.ticket; let ticket_feature = &worker.manifest().feature.ticket;
if !ticket_feature.enabled if !ticket_feature.enabled
|| !matches!( || ticket_feature.preset != TicketFeatureAccessConfig::OrchestrationControl
ticket_feature.access,
TicketFeatureAccessConfig::OrchestrationControl | TicketFeatureAccessConfig::Lifecycle
)
{ {
return; return;
} }
@ -677,8 +674,8 @@ where
if feature_config.task.enabled { if feature_config.task.enabled {
feature_registry.add_module(task_feature); feature_registry.add_module(task_feature);
} }
if feature_config.ticket.enabled || feature_config.ticket_orchestration.enabled { if feature_config.ticket.enabled {
let ticket_access = match feature_config.ticket.access { let ticket_access = match feature_config.ticket.preset {
TicketFeatureAccessConfig::ReadOnly => { TicketFeatureAccessConfig::ReadOnly => {
crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly crate::feature::builtin::ticket::TicketFeatureAccess::ReadOnly
} }
@ -697,9 +694,6 @@ where
TicketFeatureAccessConfig::Review => { TicketFeatureAccessConfig::Review => {
crate::feature::builtin::ticket::TicketFeatureAccess::Review crate::feature::builtin::ticket::TicketFeatureAccess::Review
} }
TicketFeatureAccessConfig::Lifecycle => {
crate::feature::builtin::ticket::TicketFeatureAccess::Lifecycle
}
}; };
// Ticket tools are typed operations over the current workspace Ticket backend. // Ticket tools are typed operations over the current workspace Ticket backend.
// Runtime-hosted Workers prefer the workspace API URI carried by the // Runtime-hosted Workers prefer the workspace API URI carried by the
@ -728,10 +722,9 @@ where
} }
}; };
feature_registry.add_module( feature_registry.add_module(
crate::feature::builtin::ticket::ticket_tools_feature_with_options( crate::feature::builtin::ticket::ticket_tools_feature_with_backend(
ticket_backend, ticket_backend,
feature_config.ticket.enabled.then_some(ticket_access), ticket_access,
feature_config.ticket_orchestration.enabled,
), ),
); );
} }

View File

@ -14,5 +14,5 @@ pub(crate) use session_explore::{
pub use task::{TaskFeature, task_tools_feature}; pub use task::{TaskFeature, task_tools_feature};
pub use ticket::{ pub use ticket::{
TicketFeature, TicketFeatureAccess, ticket_tools_feature, ticket_tools_feature_with_access, TicketFeature, TicketFeatureAccess, ticket_tools_feature, ticket_tools_feature_with_access,
ticket_tools_feature_with_options, ticket_tools_feature_with_backend,
}; };

View File

@ -14,11 +14,7 @@ use ticket::{
TicketIntakeSummary, TicketListQuery, TicketRef, TicketRelation, TicketRelationKind, TicketIntakeSummary, TicketListQuery, TicketRef, TicketRelation, TicketRelationKind,
TicketRelationView, TicketReview, TicketStateChange, TicketSummary, TicketRelationView, TicketReview, TicketStateChange, TicketSummary,
config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig}, config::{DEFAULT_TICKET_BACKEND_RELATIVE_PATH, TicketConfig},
tool::{ tool::{TicketToolBackend, ticket_tool_description, ticket_tools},
TICKET_BASE_READ_ONLY_TOOL_NAMES, TICKET_BASE_TOOL_NAMES,
TICKET_ORCHESTRATION_READ_ONLY_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES,
TicketToolBackend, ticket_tool_description, ticket_tools,
},
}; };
use crate::feature::{ use crate::feature::{
@ -45,11 +41,18 @@ pub enum TicketFeatureAccess {
WorkReport, WorkReport,
/// Reviewer-style access for reading Tickets and writing review/comment thread events. /// Reviewer-style access for reading Tickets and writing review/comment thread events.
Review, Review,
/// Legacy full lifecycle access retained as a migration shim.
Lifecycle,
} }
const WORKSPACE_AUTHORING_BASE_TOOL_NAMES: [&str; 10] = [ const READ_ONLY_TOOL_NAMES: [&str; 6] = [
"TicketList",
"TicketShow",
"TicketDependencyCheck",
"TicketDoctor",
"TicketRelationQuery",
"TicketOrchestrationPlanQuery",
];
const WORKSPACE_AUTHORING_TOOL_NAMES: [&str; 12] = [
"TicketCreate", "TicketCreate",
"TicketEditItem", "TicketEditItem",
"TicketList", "TicketList",
@ -60,9 +63,11 @@ const WORKSPACE_AUTHORING_BASE_TOOL_NAMES: [&str; 10] = [
"TicketClose", "TicketClose",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationRecord",
"TicketRelationQuery",
]; ];
const INTAKE_BASE_TOOL_NAMES: [&str; 8] = [ const INTAKE_TOOL_NAMES: [&str; 10] = [
"TicketCreate", "TicketCreate",
"TicketEditItem", "TicketEditItem",
"TicketList", "TicketList",
@ -71,9 +76,11 @@ const INTAKE_BASE_TOOL_NAMES: [&str; 8] = [
"TicketIntakeReady", "TicketIntakeReady",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationRecord",
"TicketRelationQuery",
]; ];
const ORCHESTRATION_CONTROL_BASE_TOOL_NAMES: [&str; 8] = [ const ORCHESTRATION_CONTROL_TOOL_NAMES: [&str; 12] = [
"TicketList", "TicketList",
"TicketShow", "TicketShow",
"TicketComment", "TicketComment",
@ -82,47 +89,42 @@ const ORCHESTRATION_CONTROL_BASE_TOOL_NAMES: [&str; 8] = [
"TicketClose", "TicketClose",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationRecord",
"TicketRelationQuery",
"TicketOrchestrationPlanRecord",
"TicketOrchestrationPlanQuery",
]; ];
const WORK_REPORT_BASE_TOOL_NAMES: [&str; 5] = [ const WORK_REPORT_TOOL_NAMES: [&str; 7] = [
"TicketList", "TicketList",
"TicketShow", "TicketShow",
"TicketComment", "TicketComment",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationQuery",
"TicketOrchestrationPlanQuery",
]; ];
const REVIEW_BASE_TOOL_NAMES: [&str; 6] = [ const REVIEW_TOOL_NAMES: [&str; 8] = [
"TicketList", "TicketList",
"TicketShow", "TicketShow",
"TicketComment", "TicketComment",
"TicketReview", "TicketReview",
"TicketDependencyCheck", "TicketDependencyCheck",
"TicketDoctor", "TicketDoctor",
"TicketRelationQuery",
"TicketOrchestrationPlanQuery",
]; ];
const RELATION_WRITE_TOOL_NAMES: [&str; 2] = ["TicketRelationRecord", "TicketRelationQuery"];
impl TicketFeatureAccess { impl TicketFeatureAccess {
pub fn base_tool_names(self) -> &'static [&'static str] { pub fn tool_names(self) -> &'static [&'static str] {
match self { match self {
Self::ReadOnly => &TICKET_BASE_READ_ONLY_TOOL_NAMES, Self::ReadOnly => &READ_ONLY_TOOL_NAMES,
Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_BASE_TOOL_NAMES, Self::WorkspaceAuthoring => &WORKSPACE_AUTHORING_TOOL_NAMES,
Self::Intake => &INTAKE_BASE_TOOL_NAMES, Self::Intake => &INTAKE_TOOL_NAMES,
Self::OrchestrationControl => &ORCHESTRATION_CONTROL_BASE_TOOL_NAMES, Self::OrchestrationControl => &ORCHESTRATION_CONTROL_TOOL_NAMES,
Self::WorkReport => &WORK_REPORT_BASE_TOOL_NAMES, Self::WorkReport => &WORK_REPORT_TOOL_NAMES,
Self::Review => &REVIEW_BASE_TOOL_NAMES, Self::Review => &REVIEW_TOOL_NAMES,
Self::Lifecycle => &TICKET_BASE_TOOL_NAMES,
}
}
pub fn orchestration_tool_names(self) -> &'static [&'static str] {
match self {
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,
} }
} }
} }
@ -167,94 +169,59 @@ pub struct TicketFeature {
record_language: Option<String>, record_language: Option<String>,
config_error: Option<String>, config_error: Option<String>,
access: TicketFeatureAccess, access: TicketFeatureAccess,
include_base_tools: bool,
include_orchestration_tools: bool,
} }
impl TicketFeature { impl TicketFeature {
pub fn new(backend_root: impl Into<PathBuf>) -> Self { pub fn new(backend_root: impl Into<PathBuf>) -> Self {
Self::new_with_access(backend_root, TicketFeatureAccess::Lifecycle) Self::new_with_access(backend_root, TicketFeatureAccess::WorkspaceAuthoring)
} }
pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self { pub fn new_with_access(backend_root: impl Into<PathBuf>, access: TicketFeatureAccess) -> Self {
Self::new_with_options(backend_root, Some(access), true)
}
pub fn new_with_options(
backend_root: impl Into<PathBuf>,
access: Option<TicketFeatureAccess>,
include_orchestration_tools: bool,
) -> Self {
Self::with_backend( Self::with_backend(
TicketFeatureBackend::Local { TicketFeatureBackend::Local {
root: backend_root.into(), root: backend_root.into(),
}, },
access, access,
include_orchestration_tools,
) )
} }
pub fn with_backend( pub fn with_backend(backend: TicketFeatureBackend, access: TicketFeatureAccess) -> Self {
backend: TicketFeatureBackend,
access: Option<TicketFeatureAccess>,
include_orchestration_tools: bool,
) -> Self {
if let TicketFeatureBackend::LocalWorkspace { workspace_root } = backend { if let TicketFeatureBackend::LocalWorkspace { workspace_root } = backend {
return Self::for_workspace_with_options( return Self::for_workspace_with_access(workspace_root, access);
workspace_root,
access,
include_orchestration_tools,
);
} }
Self { Self {
backend, backend,
record_language: None, record_language: None,
config_error: None, config_error: None,
access: access.unwrap_or(TicketFeatureAccess::Lifecycle), access,
include_base_tools: access.is_some(),
include_orchestration_tools,
} }
} }
pub fn for_workspace(workspace: impl AsRef<Path>) -> Self { pub fn for_workspace(workspace: impl AsRef<Path>) -> Self {
Self::for_workspace_with_access(workspace, TicketFeatureAccess::Lifecycle) Self::for_workspace_with_access(workspace, TicketFeatureAccess::WorkspaceAuthoring)
} }
pub fn for_workspace_with_access( pub fn for_workspace_with_access(
workspace: impl AsRef<Path>, workspace: impl AsRef<Path>,
access: TicketFeatureAccess, access: TicketFeatureAccess,
) -> Self {
Self::for_workspace_with_options(workspace, Some(access), true)
}
pub fn for_workspace_with_options(
workspace: impl AsRef<Path>,
access: Option<TicketFeatureAccess>,
include_orchestration_tools: bool,
) -> Self { ) -> Self {
let workspace = workspace.as_ref(); let workspace = workspace.as_ref();
match TicketConfig::load_workspace(workspace) { match TicketConfig::load_workspace(workspace) {
Ok(config) => { Ok(config) => {
let backend_root = config.backend_root().to_path_buf(); let backend_root = config.backend_root().to_path_buf();
let record_language = config.ticket_record_language().map(str::to_string); let record_language = config.ticket_record_language().map(str::to_string);
let mut feature = let mut feature = Self::new_with_access(backend_root, access);
Self::new_with_options(backend_root, access, include_orchestration_tools);
feature.record_language = record_language; feature.record_language = record_language;
feature feature
} }
Err(error) => { Err(error) => Self {
let access_value = access.unwrap_or(TicketFeatureAccess::Lifecycle); backend: TicketFeatureBackend::Local {
Self { root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH),
backend: TicketFeatureBackend::Local { },
root: workspace.join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH), record_language: None,
}, config_error: Some(error.to_string()),
record_language: None, access,
config_error: Some(error.to_string()), },
access: access_value,
include_base_tools: access.is_some(),
include_orchestration_tools,
}
}
} }
} }
@ -270,15 +237,8 @@ impl TicketFeature {
self.access self.access
} }
fn enabled_tool_names(&self) -> Vec<&'static str> { fn enabled_tool_names(&self) -> &'static [&'static str] {
let mut names = Vec::new(); self.access.tool_names()
if self.include_base_tools {
names.extend_from_slice(self.access.base_tool_names());
}
if self.include_orchestration_tools {
names.extend_from_slice(self.access.orchestration_tool_names());
}
names
} }
fn usable_backend_root(&self) -> Result<PathBuf, String> { fn usable_backend_root(&self) -> Result<PathBuf, String> {
@ -336,7 +296,7 @@ impl FeatureModule for TicketFeature {
let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME) let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME)
.with_description(FEATURE_DESCRIPTION); .with_description(FEATURE_DESCRIPTION);
let enabled_tool_names = self.enabled_tool_names(); let enabled_tool_names = self.enabled_tool_names();
for name in &enabled_tool_names { for name in enabled_tool_names {
descriptor = descriptor.with_tool(ToolDeclaration::new( descriptor = descriptor.with_tool(ToolDeclaration::new(
*name, *name,
ticket_tool_description(name, self.record_language.as_deref()), ticket_tool_description(name, self.record_language.as_deref()),
@ -680,12 +640,11 @@ pub fn ticket_tools_feature_with_access(
TicketFeature::for_workspace_with_access(workspace, access) TicketFeature::for_workspace_with_access(workspace, access)
} }
pub fn ticket_tools_feature_with_options( pub fn ticket_tools_feature_with_backend(
backend: impl Into<TicketFeatureBackend>, backend: impl Into<TicketFeatureBackend>,
access: Option<TicketFeatureAccess>, access: TicketFeatureAccess,
include_orchestration_tools: bool,
) -> TicketFeature { ) -> TicketFeature {
TicketFeature::with_backend(backend.into(), access, include_orchestration_tools) TicketFeature::with_backend(backend.into(), access)
} }
#[cfg(test)] #[cfg(test)]
@ -697,10 +656,6 @@ mod tests {
use std::net::TcpListener; use std::net::TcpListener;
use std::thread; use std::thread;
use tempfile::TempDir; use tempfile::TempDir;
use ticket::tool::{
TICKET_BASE_TOOL_NAMES, TICKET_ORCHESTRATION_TOOL_NAMES, TICKET_READ_ONLY_TOOL_NAMES,
TICKET_TOOL_NAMES,
};
fn make_ticket_root(root: &Path) { fn make_ticket_root(root: &Path) {
std::fs::create_dir_all(root).unwrap(); std::fs::create_dir_all(root).unwrap();
@ -732,14 +687,14 @@ mod tests {
let descriptor = feature.descriptor(); let descriptor = feature.descriptor();
assert_eq!(descriptor.id.to_string(), "builtin:ticket"); assert_eq!(descriptor.id.to_string(), "builtin:ticket");
assert_eq!(descriptor.runtime, FeatureRuntimeKind::Builtin); assert_eq!(descriptor.runtime, FeatureRuntimeKind::Builtin);
assert_eq!(descriptor.tools.len(), TICKET_TOOL_NAMES.len()); assert_eq!(descriptor.tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len());
assert_eq!( assert_eq!(
descriptor descriptor
.tools .tools
.iter() .iter()
.map(|tool| tool.name.as_str()) .map(|tool| tool.name.as_str())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
TICKET_TOOL_NAMES WORKSPACE_AUTHORING_TOOL_NAMES
); );
} }
@ -749,48 +704,33 @@ mod tests {
let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly); let feature = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::ReadOnly);
let descriptor = feature.descriptor(); let descriptor = feature.descriptor();
assert_eq!(feature.access(), TicketFeatureAccess::ReadOnly); assert_eq!(feature.access(), TicketFeatureAccess::ReadOnly);
assert_eq!(descriptor.tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len()); assert_eq!(descriptor.tools.len(), READ_ONLY_TOOL_NAMES.len());
assert_eq!( assert_eq!(
descriptor descriptor
.tools .tools
.iter() .iter()
.map(|tool| tool.name.as_str()) .map(|tool| tool.name.as_str())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
TICKET_READ_ONLY_TOOL_NAMES READ_ONLY_TOOL_NAMES
); );
} }
#[test] #[test]
fn descriptor_can_expose_base_ticket_without_orchestration_tools() { fn orchestration_control_descriptor_declares_orchestration_tools() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let feature = ticket_tools_feature_with_options( let feature = ticket_tools_feature_with_access(
temp.path(), temp.path(),
Some(TicketFeatureAccess::Lifecycle), TicketFeatureAccess::OrchestrationControl,
false,
); );
let descriptor = feature.descriptor(); let descriptor = feature.descriptor();
assert_eq!(feature.access(), TicketFeatureAccess::OrchestrationControl);
assert_eq!( assert_eq!(
descriptor descriptor
.tools .tools
.iter() .iter()
.map(|tool| tool.name.as_str()) .map(|tool| tool.name.as_str())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
TICKET_BASE_TOOL_NAMES ORCHESTRATION_CONTROL_TOOL_NAMES
);
}
#[test]
fn descriptor_can_expose_orchestration_only_tools() {
let temp = TempDir::new().unwrap();
let feature = ticket_tools_feature_with_options(temp.path(), None, true);
let descriptor = feature.descriptor();
assert_eq!(
descriptor
.tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>(),
TICKET_ORCHESTRATION_TOOL_NAMES
); );
} }
@ -798,11 +738,8 @@ mod tests {
fn semantic_access_presets_expose_role_capability_surfaces() { fn semantic_access_presets_expose_role_capability_surfaces() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let workspace_authoring = ticket_tools_feature_with_options( let workspace_authoring =
temp.path(), ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkspaceAuthoring);
Some(TicketFeatureAccess::WorkspaceAuthoring),
false,
);
let workspace_descriptor = workspace_authoring.descriptor(); let workspace_descriptor = workspace_authoring.descriptor();
let workspace_tools = workspace_descriptor let workspace_tools = workspace_descriptor
.tools .tools
@ -814,10 +751,9 @@ mod tests {
assert!(workspace_tools.contains(&"TicketQueue")); assert!(workspace_tools.contains(&"TicketQueue"));
assert!(!workspace_tools.contains(&"TicketWorkflowState")); assert!(!workspace_tools.contains(&"TicketWorkflowState"));
let orchestration = ticket_tools_feature_with_options( let orchestration = ticket_tools_feature_with_access(
temp.path(), temp.path(),
Some(TicketFeatureAccess::OrchestrationControl), TicketFeatureAccess::OrchestrationControl,
true,
); );
let orchestration_descriptor = orchestration.descriptor(); let orchestration_descriptor = orchestration.descriptor();
let orchestration_tools = orchestration_descriptor let orchestration_tools = orchestration_descriptor
@ -832,11 +768,8 @@ mod tests {
assert!(!orchestration_tools.contains(&"TicketEditItem")); assert!(!orchestration_tools.contains(&"TicketEditItem"));
assert!(!orchestration_tools.contains(&"TicketQueue")); assert!(!orchestration_tools.contains(&"TicketQueue"));
let work_report = ticket_tools_feature_with_options( let work_report =
temp.path(), ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::WorkReport);
Some(TicketFeatureAccess::WorkReport),
false,
);
let work_report_descriptor = work_report.descriptor(); let work_report_descriptor = work_report.descriptor();
let work_report_tools = work_report_descriptor let work_report_tools = work_report_descriptor
.tools .tools
@ -847,11 +780,7 @@ mod tests {
assert!(!work_report_tools.contains(&"TicketReview")); assert!(!work_report_tools.contains(&"TicketReview"));
assert!(!work_report_tools.contains(&"TicketWorkflowState")); assert!(!work_report_tools.contains(&"TicketWorkflowState"));
let review = ticket_tools_feature_with_options( let review = ticket_tools_feature_with_access(temp.path(), TicketFeatureAccess::Review);
temp.path(),
Some(TicketFeatureAccess::Review),
false,
);
let review_descriptor = review.descriptor(); let review_descriptor = review.descriptor();
let review_tools = review_descriptor let review_tools = review_descriptor
.tools .tools
@ -875,16 +804,13 @@ mod tests {
)) ))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), READ_ONLY_TOOL_NAMES.len());
assert_eq!( assert_eq!(report.reports[0].installed_tools, READ_ONLY_TOOL_NAMES);
report.reports[0].installed_tools,
TICKET_READ_ONLY_TOOL_NAMES
);
let pending_names = pending_tools let pending_names = pending_tools
.iter() .iter()
.map(|definition| definition().0.name) .map(|definition| definition().0.name)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(pending_names, TICKET_READ_ONLY_TOOL_NAMES); assert_eq!(pending_names, READ_ONLY_TOOL_NAMES);
for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES { for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES {
assert!( assert!(
!report.reports[0] !report.reports[0]
@ -924,11 +850,8 @@ language = "Japanese"
.with_module(feature) .with_module(feature)
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_READ_ONLY_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), READ_ONLY_TOOL_NAMES.len());
assert_eq!( assert_eq!(report.reports[0].installed_tools, READ_ONLY_TOOL_NAMES);
report.reports[0].installed_tools,
TICKET_READ_ONLY_TOOL_NAMES
);
let description = pending_tool_description(&pending_tools, "TicketShow"); let description = pending_tool_description(&pending_tools, "TicketShow");
assert!(description.contains("Ticket record language: Japanese")); assert!(description.contains("Ticket record language: Japanese"));
assert!(description.contains("distinct from worker.language")); assert!(description.contains("distinct from worker.language"));
@ -936,7 +859,7 @@ language = "Japanese"
} }
#[test] #[test]
fn lifecycle_installation_exposes_lifecycle_tools() { fn workspace_authoring_installation_exposes_authoring_tools() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH)); make_ticket_root(&temp.path().join(DEFAULT_TICKET_BACKEND_RELATIVE_PATH));
let mut pending_tools = Vec::new(); let mut pending_tools = Vec::new();
@ -944,36 +867,31 @@ language = "Japanese"
let report = FeatureRegistryBuilder::new() let report = FeatureRegistryBuilder::new()
.with_module(ticket_tools_feature_with_access( .with_module(ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::Lifecycle, TicketFeatureAccess::WorkspaceAuthoring,
)) ))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len());
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); let installed = report.reports[0]
for name in ticket::tool::TICKET_MUTATING_TOOL_NAMES { .installed_tools
assert!( .iter()
report.reports[0] .map(String::as_str)
.installed_tools .collect::<Vec<_>>();
.iter() assert_eq!(installed, WORKSPACE_AUTHORING_TOOL_NAMES);
.any(|tool| tool == name) assert!(installed.iter().any(|tool| *tool == "TicketCreate"));
); assert!(installed.iter().any(|tool| *tool == "TicketEditItem"));
} assert!(installed.iter().any(|tool| *tool == "TicketQueue"));
assert!(!installed.iter().any(|tool| *tool == "TicketIntakeReady"));
assert!(!installed.iter().any(|tool| *tool == "TicketWorkflowState"));
assert!( assert!(
report.reports[0] !installed
.installed_tools
.iter() .iter()
.any(|tool| tool == "TicketIntakeReady") .any(|tool| *tool == "TicketOrchestrationPlanRecord")
);
assert!(
report.reports[0]
.installed_tools
.iter()
.any(|tool| tool == "TicketWorkflowState")
); );
} }
#[test] #[test]
fn lifecycle_ticket_role_style_context_exposes_ticket_language_guidance() { fn workspace_authoring_context_exposes_ticket_language_guidance() {
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
write_ticket_config( write_ticket_config(
temp.path(), temp.path(),
@ -988,12 +906,15 @@ language = "Japanese"
let report = FeatureRegistryBuilder::new() let report = FeatureRegistryBuilder::new()
.with_module(ticket_tools_feature_with_access( .with_module(ticket_tools_feature_with_access(
temp.path(), temp.path(),
TicketFeatureAccess::Lifecycle, TicketFeatureAccess::WorkspaceAuthoring,
)) ))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len());
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); assert_eq!(
report.reports[0].installed_tools,
WORKSPACE_AUTHORING_TOOL_NAMES
);
let description = pending_tool_description(&pending_tools, "TicketComment"); let description = pending_tool_description(&pending_tools, "TicketComment");
assert!(description.contains("Ticket record language: Japanese")); assert!(description.contains("Ticket record language: Japanese"));
assert!(description.contains("durable Ticket record and Ticket tool body text")); assert!(description.contains("durable Ticket record and Ticket tool body text"));
@ -1011,10 +932,13 @@ language = "Japanese"
.with_module(ticket_tools_feature(temp.path())) .with_module(ticket_tools_feature(temp.path()))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len());
assert_eq!(report.reports.len(), 1); assert_eq!(report.reports.len(), 1);
assert!(report.reports[0].installed); assert!(report.reports[0].installed);
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); assert_eq!(
report.reports[0].installed_tools,
WORKSPACE_AUTHORING_TOOL_NAMES
);
assert!(report.reports[0].skipped.is_empty()); assert!(report.reports[0].skipped.is_empty());
} }
@ -1046,7 +970,7 @@ profile = "project:coder"
.with_module(feature) .with_module(feature)
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len());
assert!(report.reports[0].diagnostics.is_empty()); assert!(report.reports[0].diagnostics.is_empty());
} }
@ -1132,8 +1056,11 @@ provider = "github"
.with_module(ticket_tools_feature(temp.path())) .with_module(ticket_tools_feature(temp.path()))
.install_into_pending(&mut pending_tools, &mut hooks); .install_into_pending(&mut pending_tools, &mut hooks);
assert_eq!(pending_tools.len(), TICKET_TOOL_NAMES.len()); assert_eq!(pending_tools.len(), WORKSPACE_AUTHORING_TOOL_NAMES.len());
assert_eq!(report.reports[0].installed_tools, TICKET_TOOL_NAMES); assert_eq!(
report.reports[0].installed_tools,
WORKSPACE_AUTHORING_TOOL_NAMES
);
assert!(report.reports[0].diagnostics.is_empty()); assert!(report.reports[0].diagnostics.is_empty());
assert!(!root.join("open").exists()); assert!(!root.join("open").exists());
assert!(!root.join("pending").exists()); assert!(!root.join("pending").exists());

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = false; }; workers = { enabled = false; };
ticket_orchestration = { enabled = false; }; ticket = { enabled = true; preset = "work_report"; };
}; };
} }

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = true; }; workers = { enabled = true; };
ticket_orchestration = { enabled = false; }; ticket = { enabled = true; preset = "workspace_authoring"; };
}; };
} }

View File

@ -26,8 +26,7 @@ feature = {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = true; }; workers = { enabled = true; };
ticket = { enabled = true; access = "lifecycle"; }; ticket = { enabled = true; preset = "workspace_authoring"; };
ticket_orchestration = { enabled = false; };
}; };
memory = { memory = {

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = false; }; workers = { enabled = false; };
ticket_orchestration = { enabled = false; }; ticket = { enabled = true; preset = "intake"; };
}; };
} }

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = true; }; workers = { enabled = true; };
ticket_orchestration = { enabled = true; }; ticket = { enabled = true; preset = "orchestration_control"; };
}; };
} }

View File

@ -8,6 +8,6 @@ import "./default.dcdl" // {
memory = { enabled = true; }; memory = { enabled = true; };
web = { enabled = true; }; web = { enabled = true; };
workers = { enabled = false; }; workers = { enabled = false; };
ticket_orchestration = { enabled = false; }; ticket = { enabled = true; preset = "review"; };
}; };
} }