ticket: collapse ticket access into one feature
This commit is contained in:
parent
2c01e7672b
commit
064965d350
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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"; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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"; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user