ticket: collapse ticket access into one feature
This commit is contained in:
@@ -87,8 +87,6 @@ pub struct FeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub ticket: Option<TicketFeatureConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub ticket_orchestration: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub plugins: Option<FeatureFlagConfigPartial>,
|
||||
}
|
||||
|
||||
@@ -100,11 +98,6 @@ impl FeatureConfigPartial {
|
||||
web: merge_option(self.web, other.web, FeatureFlagConfigPartial::merge),
|
||||
workers: merge_option(self.workers, other.workers, FeatureFlagConfigPartial::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),
|
||||
}
|
||||
}
|
||||
@@ -128,10 +121,6 @@ impl FeatureFlagConfigPartial {
|
||||
pub struct TicketFeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
/// Legacy access field. Prefer `preset` for new profile/DCDL authoring.
|
||||
#[serde(default)]
|
||||
pub access: Option<TicketFeatureAccessConfig>,
|
||||
/// Semantic Ticket access preset for profile/DCDL authoring.
|
||||
#[serde(default)]
|
||||
pub preset: Option<TicketFeatureAccessConfig>,
|
||||
}
|
||||
@@ -140,7 +129,6 @@ impl TicketFeatureConfigPartial {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Self {
|
||||
enabled: other.enabled.or(self.enabled),
|
||||
access: other.access.or(self.access),
|
||||
preset: other.preset.or(self.preset),
|
||||
}
|
||||
}
|
||||
@@ -163,10 +151,6 @@ impl From<FeatureConfigPartial> for FeatureConfig {
|
||||
.ticket
|
||||
.map(TicketFeatureConfig::from)
|
||||
.unwrap_or_default(),
|
||||
ticket_orchestration: value
|
||||
.ticket_orchestration
|
||||
.map(FeatureFlagConfig::from)
|
||||
.unwrap_or_default(),
|
||||
plugins: value
|
||||
.plugins
|
||||
.map(FeatureFlagConfig::from)
|
||||
@@ -195,7 +179,7 @@ impl From<TicketFeatureConfigPartial> for TicketFeatureConfig {
|
||||
fn from(value: TicketFeatureConfigPartial) -> Self {
|
||||
Self {
|
||||
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 {
|
||||
Self {
|
||||
enabled: Some(value.enabled),
|
||||
access: Some(value.access),
|
||||
preset: None,
|
||||
preset: Some(value.preset),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +201,6 @@ impl From<FeatureConfig> for FeatureConfigPartial {
|
||||
web: Some(value.web.into()),
|
||||
workers: Some(value.workers.into()),
|
||||
ticket: Some(value.ticket.into()),
|
||||
ticket_orchestration: Some(value.ticket_orchestration.into()),
|
||||
plugins: Some(value.plugins.into()),
|
||||
}
|
||||
}
|
||||
@@ -1764,7 +1746,6 @@ worker_max_turns = 7
|
||||
assert!(!manifest.feature.web.enabled);
|
||||
assert!(!manifest.feature.workers.enabled);
|
||||
assert!(!manifest.feature.ticket.enabled);
|
||||
assert!(!manifest.feature.ticket_orchestration.enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1776,10 +1757,7 @@ enabled = true
|
||||
|
||||
[feature.ticket]
|
||||
enabled = true
|
||||
access = "read_only"
|
||||
|
||||
[feature.ticket_orchestration]
|
||||
enabled = true
|
||||
preset = "read_only"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1810,10 +1788,9 @@ enabled = true
|
||||
assert!(manifest.feature.task.enabled);
|
||||
assert!(manifest.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
manifest.feature.ticket.access,
|
||||
manifest.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::ReadOnly
|
||||
);
|
||||
assert!(manifest.feature.ticket_orchestration.enabled);
|
||||
assert!(!manifest.feature.memory.enabled);
|
||||
}
|
||||
|
||||
@@ -1826,14 +1803,14 @@ enabled = true
|
||||
|
||||
[feature.ticket]
|
||||
enabled = true
|
||||
access = "read_only"
|
||||
preset = "read_only"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let upper = WorkerManifestConfig::from_toml(
|
||||
r#"
|
||||
[feature.ticket]
|
||||
access = "lifecycle"
|
||||
preset = "orchestration_control"
|
||||
|
||||
[feature.web]
|
||||
enabled = true
|
||||
@@ -1868,8 +1845,8 @@ enabled = true
|
||||
assert!(manifest.feature.memory.enabled);
|
||||
assert!(manifest.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
manifest.feature.ticket.access,
|
||||
TicketFeatureAccessConfig::Lifecycle
|
||||
manifest.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::OrchestrationControl
|
||||
);
|
||||
assert!(manifest.feature.web.enabled);
|
||||
assert!(!manifest.feature.workers.enabled);
|
||||
|
||||
@@ -115,8 +115,6 @@ pub struct FeatureConfig {
|
||||
#[serde(default)]
|
||||
pub ticket: TicketFeatureConfig,
|
||||
#[serde(default)]
|
||||
pub ticket_orchestration: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: FeatureFlagConfig,
|
||||
}
|
||||
|
||||
@@ -128,7 +126,6 @@ impl Default for FeatureConfig {
|
||||
web: FeatureFlagConfig::disabled(),
|
||||
workers: FeatureFlagConfig::disabled(),
|
||||
ticket: TicketFeatureConfig::default(),
|
||||
ticket_orchestration: FeatureFlagConfig::disabled(),
|
||||
plugins: FeatureFlagConfig::disabled(),
|
||||
}
|
||||
}
|
||||
@@ -160,18 +157,15 @@ impl Default for FeatureFlagConfig {
|
||||
pub struct TicketFeatureConfig {
|
||||
#[serde(default)]
|
||||
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)]
|
||||
pub access: TicketFeatureAccessConfig,
|
||||
pub preset: TicketFeatureAccessConfig,
|
||||
}
|
||||
|
||||
impl Default for TicketFeatureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
access: TicketFeatureAccessConfig::Lifecycle,
|
||||
preset: TicketFeatureAccessConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,13 +179,11 @@ pub enum TicketFeatureAccessConfig {
|
||||
OrchestrationControl,
|
||||
WorkReport,
|
||||
Review,
|
||||
/// Legacy broad mutation preset retained as a migration shim.
|
||||
Lifecycle,
|
||||
}
|
||||
|
||||
impl Default for TicketFeatureAccessConfig {
|
||||
fn default() -> Self {
|
||||
Self::Lifecycle
|
||||
Self::WorkspaceAuthoring
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -894,7 +894,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
Some(value)
|
||||
}
|
||||
@@ -908,7 +907,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
Some(value)
|
||||
}
|
||||
@@ -922,7 +920,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
Some(value)
|
||||
}
|
||||
@@ -936,7 +933,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
Some(value)
|
||||
}
|
||||
@@ -950,7 +946,6 @@ fn builtin_profile_artifact(label: &str) -> Option<serde_json::Value> {
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
Some(value)
|
||||
}
|
||||
@@ -976,8 +971,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value {
|
||||
"memory": { "enabled": true },
|
||||
"web": { "enabled": true },
|
||||
"workers": { "enabled": true },
|
||||
"ticket": { "enabled": true, "access": "lifecycle" },
|
||||
"ticket_orchestration": { "enabled": false }
|
||||
"ticket": { "enabled": true, "preset": "workspace_authoring" }
|
||||
},
|
||||
"memory": {
|
||||
"extract_threshold": 50000,
|
||||
@@ -1004,7 +998,6 @@ fn apply_role_profile(
|
||||
memory: bool,
|
||||
web: bool,
|
||||
workers: bool,
|
||||
ticket_orchestration: bool,
|
||||
) {
|
||||
value["slug"] = serde_json::Value::String(slug.to_string());
|
||||
value["description"] = serde_json::Value::String(description.to_string());
|
||||
@@ -1018,14 +1011,12 @@ fn apply_role_profile(
|
||||
"orchestrator" => TicketFeatureAccessConfig::OrchestrationControl,
|
||||
"coder" => TicketFeatureAccessConfig::WorkReport,
|
||||
"reviewer" => TicketFeatureAccessConfig::Review,
|
||||
_ => TicketFeatureAccessConfig::Lifecycle,
|
||||
_ => TicketFeatureAccessConfig::WorkspaceAuthoring,
|
||||
};
|
||||
value["feature"]["ticket"] = serde_json::json!({
|
||||
"enabled": true,
|
||||
"preset": ticket_access,
|
||||
});
|
||||
value["feature"]["ticket_orchestration"] =
|
||||
serde_json::json!({ "enabled": ticket_orchestration });
|
||||
}
|
||||
|
||||
fn reject_manifest_shaped_profile(value: &serde_json::Value) -> Result<(), ProfileError> {
|
||||
@@ -1452,10 +1443,9 @@ mod tests {
|
||||
assert!(companion.web.is_some());
|
||||
assert!(companion.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
companion.feature.ticket.access,
|
||||
companion.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::WorkspaceAuthoring
|
||||
);
|
||||
assert!(!companion.feature.ticket_orchestration.enabled);
|
||||
assert_eq!(
|
||||
companion.compaction.as_ref().unwrap().threshold,
|
||||
Some(240000)
|
||||
@@ -1478,7 +1468,7 @@ mod tests {
|
||||
assert!(!intake.feature.workers.enabled);
|
||||
assert!(intake.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
intake.feature.ticket.access,
|
||||
intake.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::Intake
|
||||
);
|
||||
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!(intake.web.is_some());
|
||||
assert!(intake.compaction.is_some());
|
||||
assert!(!intake.feature.ticket_orchestration.enabled);
|
||||
|
||||
let orchestrator = resolve("orchestrator");
|
||||
assert!(orchestrator.feature.task.enabled);
|
||||
assert!(orchestrator.feature.workers.enabled);
|
||||
assert!(orchestrator.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
orchestrator.feature.ticket.access,
|
||||
orchestrator.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::OrchestrationControl
|
||||
);
|
||||
assert!(orchestrator.feature.ticket_orchestration.enabled);
|
||||
assert!(orchestrator.scope.allow.is_empty());
|
||||
assert!(orchestrator.delegation_scope.allow.is_empty());
|
||||
assert_eq!(
|
||||
@@ -1516,20 +1504,18 @@ mod tests {
|
||||
assert!(coder.compaction.is_some());
|
||||
assert!(coder.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
coder.feature.ticket.access,
|
||||
coder.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::WorkReport
|
||||
);
|
||||
assert!(!coder.feature.ticket_orchestration.enabled);
|
||||
|
||||
let reviewer = resolve("reviewer");
|
||||
assert!(reviewer.feature.task.enabled);
|
||||
assert!(!reviewer.feature.workers.enabled);
|
||||
assert!(reviewer.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
reviewer.feature.ticket.access,
|
||||
reviewer.feature.ticket.preset,
|
||||
TicketFeatureAccessConfig::Review
|
||||
);
|
||||
assert!(!reviewer.feature.ticket_orchestration.enabled);
|
||||
assert!(reviewer.scope.allow.is_empty());
|
||||
assert!(reviewer.delegation_scope.allow.is_empty());
|
||||
assert_eq!(reviewer.model.ref_.as_deref(), Some("codex-oauth/gpt-5.5"));
|
||||
@@ -1678,10 +1664,7 @@ enabled = true
|
||||
|
||||
[feature.ticket]
|
||||
enabled = true
|
||||
access = "read_only"
|
||||
|
||||
[feature.ticket_orchestration]
|
||||
enabled = false
|
||||
preset = "read_only"
|
||||
"#,
|
||||
);
|
||||
let workspace = tmp.path().join("workspace");
|
||||
@@ -1700,10 +1683,9 @@ enabled = false
|
||||
assert!(resolved.manifest.feature.workers.enabled);
|
||||
assert!(resolved.manifest.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
resolved.manifest.feature.ticket.access,
|
||||
resolved.manifest.feature.ticket.preset,
|
||||
crate::TicketFeatureAccessConfig::ReadOnly
|
||||
);
|
||||
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
|
||||
assert_eq!(
|
||||
resolved.manifest.delegation_scope.allow[0].target,
|
||||
workspace
|
||||
@@ -1791,10 +1773,9 @@ worker_context_max_tokens = 68000
|
||||
assert!(resolved.manifest.session.record_event_trace);
|
||||
assert!(resolved.manifest.feature.ticket.enabled);
|
||||
assert_eq!(
|
||||
resolved.manifest.feature.ticket.access,
|
||||
crate::TicketFeatureAccessConfig::Lifecycle
|
||||
resolved.manifest.feature.ticket.preset,
|
||||
crate::TicketFeatureAccessConfig::WorkspaceAuthoring
|
||||
);
|
||||
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
|
||||
assert_eq!(
|
||||
resolved.profile.as_ref().unwrap().name.as_deref(),
|
||||
Some("default")
|
||||
|
||||
Reference in New Issue
Block a user