use std::collections::BTreeSet;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{bail, Result};
use clap::{ArgMatches, Command as ClapCommand};
use serde_json::Value;
use crate::commands::describe::CommandDescriptor;
use crate::handoff::GitState;
use crate::mcp::protocol::Tool;
use crate::output::OutputFormat;
use crate::paths::state::StateLayout;
#[cfg(any(feature = "extension-backlog", test))]
pub(crate) mod adapter;
#[cfg(feature = "extension-backlog")]
pub(crate) mod backlog;
#[cfg(feature = "extension-backlog")]
pub(crate) mod backlog_commands;
#[cfg(any(feature = "extension-backlog", test))]
pub(crate) mod backlog_config;
#[cfg(any(feature = "extension-backlog", test))]
mod backlog_state;
#[cfg(feature = "extension-codemap")]
pub(crate) mod codemap;
#[cfg(feature = "extension-codemap")]
pub(crate) mod codemap_commands;
#[cfg(feature = "extension-backlog")]
mod dispatch;
#[cfg(any(feature = "extension-backlog", test))]
mod dispatch_state;
pub(crate) mod issue_refs;
pub(crate) mod work_queue;
pub(crate) use work_queue::{
BacklogRef, WorkQueueCacheView, WorkQueueSnapshot, WorkQueueSnapshotItem, WorkQueueSummaryItem,
};
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct HealthDiagnostic {
pub check: &'static str,
pub severity: &'static str,
pub file: String,
pub message: String,
pub details: Option<serde_json::Value>,
}
pub(crate) struct RadarEvaluationBucket {
pub status: &'static str,
pub summary: String,
pub evidence: Vec<String>,
}
pub(crate) struct RadarApprovalStep {
pub id: &'static str,
pub question: &'static str,
pub recommended_answer: &'static str,
pub recommendation: String,
pub evidence: Vec<String>,
}
pub(crate) struct RadarWorkflowGuidance {
pub evaluation: RadarEvaluationBucket,
pub approval_steps: Vec<RadarApprovalStep>,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RadarBehavioralDriftStatus {
Aligned,
Drift,
NoSignal,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct RadarBehavioralDriftSignal {
pub id: &'static str,
pub status: RadarBehavioralDriftStatus,
pub summary: String,
pub evidence: Vec<String>,
pub recommended_correction: Option<String>,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct StartupContext<'a> {
pub(crate) layout: &'a StateLayout,
pub(crate) repo_root: &'a Path,
pub(crate) locality_id: &'a str,
pub(crate) allow_cached_work: bool,
}
#[cfg_attr(not(feature = "extension-backlog"), allow(dead_code))]
pub(crate) struct RadarContext<'a> {
pub(crate) startup: StartupContext<'a>,
pub(crate) git: Option<&'a GitState>,
pub(crate) handoff_title: &'a str,
pub(crate) handoff_immediate_actions: &'a [String],
pub(crate) active_session_id: Option<&'a str>,
}
pub(crate) trait Extension {
fn name(&self) -> &'static str;
fn command_groups(&self) -> &'static [&'static str];
fn cli_command(&self) -> Option<ClapCommand> {
None
}
fn dispatch_cli(
&self,
_subcommand_name: &str,
_matches: &ArgMatches,
_output: OutputFormat,
) -> Option<Result<ExitCode>> {
None
}
fn mcp_tools(&self, _commands: &[CommandDescriptor]) -> Vec<Tool> {
Vec::new()
}
fn dispatch_mcp(&self, _tool_name: &str, _args: &Value) -> Option<Result<Value>> {
None
}
fn health_diagnostics(
&self,
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
Ok(Vec::new())
}
fn radar_workflow_guidance(
&self,
_ctx: &StartupContext<'_>,
) -> Result<Option<RadarWorkflowGuidance>> {
Ok(None)
}
fn radar_behavioral_drift_signals(
&self,
_ctx: &RadarContext<'_>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
Ok(Vec::new())
}
fn enrich_pod_status(
&self,
_pod_name: &str,
_locality_id: &str,
_profile: &str,
_shared_root: &Path,
) -> Option<Vec<(String, String)>> {
None
}
}
#[cfg(feature = "extension-backlog")]
pub(crate) trait WorkflowExtension: Extension {
fn work_queue_refresh_hint(&self, provider: Option<&str>, repo: Option<&str>)
-> Option<String>;
fn refresh_work_queue_cache(
&self,
repo_root: &Path,
layout: &StateLayout,
) -> Option<Result<(String, usize)>>;
fn load_work_queue_snapshot(&self, layout: &StateLayout) -> Result<Option<WorkQueueSnapshot>>;
fn observe_next_step(
&self,
ctx: &StartupContext<'_>,
) -> Result<Option<dispatch::NextStepObservation>>;
fn ensure_assignment(
&self,
ctx: &StartupContext<'_>,
owner: dispatch::AssignmentOwner<'_>,
) -> Result<dispatch::AssignmentOutcome>;
fn load_session_assignment(
&self,
ctx: &StartupContext<'_>,
session_id: &str,
) -> Result<Option<dispatch::AssignmentView>>;
fn load_branch_assignment(
&self,
ctx: &StartupContext<'_>,
branch: &str,
) -> Result<Option<dispatch::AssignmentView>>;
fn on_session_started(
&self,
ctx: &dispatch::SessionBoundaryContext<'_>,
) -> Result<()>;
fn on_session_cleared(
&self,
ctx: &dispatch::SessionBoundaryContext<'_>,
) -> Result<()>;
fn resolve_assignment_references(
&self,
ctx: &StartupContext<'_>,
assignment: &dispatch::AssignmentView,
) -> Result<Vec<dispatch::StartupAlert>>;
}
#[cfg(all(feature = "extension-backlog", feature = "extension-codemap"))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
vec![&backlog::BACKLOG_EXTENSION, &codemap::CODEMAP_EXTENSION]
}
#[cfg(all(feature = "extension-backlog", not(feature = "extension-codemap")))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
vec![&backlog::BACKLOG_EXTENSION]
}
#[cfg(all(not(feature = "extension-backlog"), feature = "extension-codemap"))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
vec![&codemap::CODEMAP_EXTENSION]
}
#[cfg(all(not(feature = "extension-backlog"), not(feature = "extension-codemap")))]
pub(crate) fn registered() -> Vec<&'static dyn Extension> {
Vec::new()
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn registered_workflow() -> Vec<&'static dyn WorkflowExtension> {
vec![&backlog::BACKLOG_EXTENSION]
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn workflow_command_groups() -> Vec<Vec<&'static str>> {
registered_workflow()
.into_iter()
.map(|extension| extension.command_groups().to_vec())
.collect()
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn workflow_command_groups() -> Vec<Vec<&'static str>> {
Vec::new()
}
#[cfg(test)]
pub(crate) fn owned_command_groups() -> Vec<&'static str> {
registered()
.into_iter()
.flat_map(|extension| extension.command_groups().iter().copied())
.collect()
}
pub(crate) fn augment_clap(mut command: ClapCommand) -> ClapCommand {
for extension in registered() {
debug_assert!(!extension.name().is_empty());
debug_assert!(!extension.command_groups().is_empty());
if let Some(subcommand) = extension.cli_command() {
command = command.subcommand(subcommand);
}
}
command
}
pub(crate) fn dispatch_cli(
subcommand_name: &str,
matches: &ArgMatches,
output: OutputFormat,
) -> Option<Result<ExitCode>> {
for extension in registered() {
if let Some(result) = extension.dispatch_cli(subcommand_name, matches, output) {
return Some(result);
}
}
None
}
pub(crate) fn build_mcp_tools(commands: &[CommandDescriptor]) -> Vec<Tool> {
let mut tools = Vec::new();
for extension in registered() {
debug_assert!(!extension.name().is_empty());
debug_assert!(!extension.command_groups().is_empty());
tools.extend(extension.mcp_tools(commands));
}
tools
}
pub(crate) fn dispatch_mcp(tool_name: &str, args: &Value) -> Option<Result<Value>> {
for extension in registered() {
if let Some(report) = extension.dispatch_mcp(tool_name, args) {
return Some(report);
}
}
None
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn work_queue_refresh_hint(
provider: Option<&str>,
repo: Option<&str>,
) -> Option<String> {
for extension in registered_workflow() {
if let Some(hint) = extension.work_queue_refresh_hint(provider, repo) {
return Some(hint);
}
}
None
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn work_queue_refresh_hint(
_provider: Option<&str>,
_repo: Option<&str>,
) -> Option<String> {
None
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn refresh_work_queue_cache(
repo_root: &Path,
layout: &StateLayout,
) -> Option<Result<(String, usize)>> {
for extension in registered_workflow() {
if let Some(result) = extension.refresh_work_queue_cache(repo_root, layout) {
return Some(result);
}
}
None
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn refresh_work_queue_cache(
_repo_root: &Path,
_layout: &StateLayout,
) -> Option<Result<(String, usize)>> {
None
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn load_work_queue_snapshot(layout: &StateLayout) -> Result<Option<WorkQueueSnapshot>> {
for extension in registered_workflow() {
if let Some(snapshot) = extension.load_work_queue_snapshot(layout)? {
return Ok(Some(snapshot));
}
}
Ok(None)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn load_work_queue_snapshot(_layout: &StateLayout) -> Result<Option<WorkQueueSnapshot>> {
Ok(None)
}
pub(crate) fn load_work_queue_view(
layout: &StateLayout,
limit: usize,
) -> Result<WorkQueueCacheView> {
let snapshot = load_work_queue_snapshot(layout)?;
Ok(work_queue::cache_view_from_snapshot(
layout,
snapshot.as_ref(),
limit,
))
}
pub(crate) fn load_work_queue_view_from_snapshot(
layout: &StateLayout,
snapshot: Option<&WorkQueueSnapshot>,
limit: usize,
) -> WorkQueueCacheView {
work_queue::cache_view_from_snapshot(layout, snapshot, limit)
}
pub(crate) fn is_stale_work_queue_snapshot(snapshot: &WorkQueueSnapshot, now_epoch_s: u64) -> bool {
work_queue::is_snapshot_stale(snapshot, now_epoch_s)
}
pub(crate) fn should_revalidate_work_queue_on_refresh(snapshot: &WorkQueueSnapshot) -> bool {
snapshot.revalidate_on_refresh
}
#[allow(clippy::type_complexity)]
#[cfg(feature = "extension-backlog")]
pub(crate) fn prepare_startup_extension(
ctx: &StartupContext<'_>,
active_session_id: Option<&str>,
branch: Option<&str>,
_worktree: &str,
landed_trunk: Option<&str>,
) -> Result<(
Value,
Vec<(&'static str, &'static str, String)>,
bool,
Option<String>,
Option<String>,
)> {
let pre_assignment_snapshot = match active_session_id {
Some(session_id) => dispatch::load_session_assignment(ctx, session_id)?,
None => match branch {
Some(branch) if !branch.is_empty() => dispatch::load_branch_assignment(ctx, branch)?,
_ => None,
},
};
let assignment_outcome = match active_session_id {
Some(session_id) => dispatch::ensure_assignment(
ctx,
dispatch::AssignmentOwner::Session {
session_id,
branch: branch.and_then(|current| (current != "HEAD").then_some(current)),
},
)?,
None => match (landed_trunk, branch) {
(Some(trunk), Some(branch)) => dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: Some(format!(
"branch `{branch}` is already landed on local `origin/{trunk}`; \
local redispatch is skipped"
)),
next_step: None,
assignment: None,
},
(None, Some(branch)) if !branch.is_empty() => dispatch::ensure_assignment(
ctx,
dispatch::AssignmentOwner::PreSessionBranch { branch },
)?,
_ => dispatch::AssignmentOutcome {
status: dispatch::AssignmentStatus::Skipped,
reason: Some(
"directory substrate has no pre-session work stream identity; choose the next item explicitly or start a session first"
.to_owned(),
),
next_step: None,
assignment: None,
},
},
};
let startup_payload = dispatch::build_startup_payload(ctx, &assignment_outcome)?;
let drives_needs_input = assignment_outcome.assignment.is_some()
|| (startup_payload.next_step.status == dispatch::NextStepStatus::NeedsInput
&& startup_payload.next_step.source == dispatch::NextStepSource::BacklogAdapter);
let boundary_stop_reason = (startup_payload.next_step.status
== dispatch::NextStepStatus::NeedsInput)
.then(|| startup_payload.next_step.reason.clone())
.flatten();
let continuity_note = if let Some(observation) = &startup_payload.next_step.observation {
let observed_ref = if observation.item.ccd_id != 0 {
format!("ccd#{}", observation.item.ccd_id)
} else if observation.item.github_issue_number != 0 {
format!("GH#{}", observation.item.github_issue_number)
} else {
observation.item.title.clone()
};
Some(format!(
"Extension-owned next step is currently `{observed_ref}`."
))
} else if assignment_outcome.assignment.is_some() {
Some("A local assignment is already active for this session context.".to_owned())
} else {
None
};
let mut alerts = Vec::new();
let ref_check_assignment = assignment_outcome
.assignment
.as_ref()
.or(pre_assignment_snapshot.as_ref());
if let Some(assignment) = ref_check_assignment {
for alert in dispatch::resolve_assignment_references(ctx, assignment)? {
let severity = match alert.severity {
dispatch::StartupAlertSeverity::Warning => "warning",
};
alerts.push((alert.check, severity, alert.message));
}
}
if let (Some(current_branch), Some(session_id), Some(assignment)) = (
branch,
active_session_id,
assignment_outcome.assignment.as_ref(),
) {
if !session_id.is_empty() && current_branch != "HEAD" {
if let Some(recorded_branch) = assignment.branch.as_ref() {
if recorded_branch != current_branch {
alerts.push((
"session_branch_drift",
"warning",
format!(
"session assignment was created on branch `{recorded_branch}` \
but current branch is `{current_branch}`"
),
));
}
}
}
}
Ok((
serde_json::to_value(startup_payload)?,
alerts,
drives_needs_input,
boundary_stop_reason,
continuity_note,
))
}
#[allow(clippy::type_complexity)]
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn prepare_startup_extension(
_ctx: &StartupContext<'_>,
active_session_id: Option<&str>,
branch: Option<&str>,
_worktree: &str,
landed_trunk: Option<&str>,
) -> Result<(
Value,
Vec<(&'static str, &'static str, String)>,
bool,
Option<String>,
Option<String>,
)> {
let assignment_reason = match (active_session_id, landed_trunk, branch) {
(Some(_), _, _) => None,
(None, Some(trunk), Some(branch)) => Some(format!(
"branch `{branch}` is already landed on local `origin/{trunk}`; \
local redispatch is skipped"
)),
(None, None, Some(branch)) if !branch.is_empty() => None,
_ => Some(
"directory substrate has no pre-session work stream identity; choose the next item explicitly or start a session first"
.to_owned(),
),
};
let next_step_reason = assignment_reason.clone().unwrap_or_else(|| {
"no extension-owned next-step observation is available; choose the next item explicitly or use a neutral session name"
.to_owned()
});
let mut next_step = serde_json::Map::new();
next_step.insert("status".to_owned(), Value::String("needs_input".to_owned()));
next_step.insert(
"source".to_owned(),
Value::String("explicit_actor_input".to_owned()),
);
next_step.insert("reason".to_owned(), Value::String(next_step_reason.clone()));
let mut assignment = serde_json::Map::new();
assignment.insert("status".to_owned(), Value::String("skipped".to_owned()));
if let Some(reason) = assignment_reason {
assignment.insert("reason".to_owned(), Value::String(reason));
}
let mut payload = serde_json::Map::new();
payload.insert("next_step".to_owned(), Value::Object(next_step));
payload.insert("assignment".to_owned(), Value::Object(assignment));
Ok((
Value::Object(payload),
Vec::new(),
false,
Some(next_step_reason),
None,
))
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn resolve_active_work_target(
ctx: &StartupContext<'_>,
active_session_id: Option<&str>,
branch: Option<&str>,
) -> Result<Option<(BacklogRef, u64, u64, String)>> {
Ok(
dispatch::resolve_active_assignment(ctx, active_session_id, branch)?.map(|assignment| {
(
assignment.backlog_ref,
assignment.ccd_id,
assignment.github_issue_number,
assignment.title,
)
}),
)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn resolve_active_work_target(
_ctx: &StartupContext<'_>,
_active_session_id: Option<&str>,
_branch: Option<&str>,
) -> Result<Option<(BacklogRef, u64, u64, String)>> {
Ok(None)
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn on_session_started(
layout: &StateLayout,
_repo_root: &Path,
locality_id: &str,
session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
let ctx = dispatch::SessionBoundaryContext {
layout,
locality_id,
session_id,
};
dispatch::on_session_started(&ctx)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn on_session_started(
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
_session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
Ok(())
}
#[cfg(feature = "extension-backlog")]
pub(crate) fn on_session_cleared(
layout: &StateLayout,
_repo_root: &Path,
locality_id: &str,
session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
let ctx = dispatch::SessionBoundaryContext {
layout,
locality_id,
session_id,
};
dispatch::on_session_cleared(&ctx)
}
#[cfg(not(feature = "extension-backlog"))]
pub(crate) fn on_session_cleared(
_layout: &StateLayout,
_repo_root: &Path,
_locality_id: &str,
_session_id: &str,
_pod: Option<(&str, &Path)>,
) -> Result<()> {
Ok(())
}
pub(crate) fn health_diagnostics(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
) -> Result<Vec<HealthDiagnostic>> {
let mut all = Vec::new();
for extension in registered() {
all.extend(extension.health_diagnostics(layout, repo_root, locality_id)?);
}
Ok(all)
}
pub(crate) fn radar_workflow_guidance(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
allow_cached_work: bool,
) -> Result<Option<RadarWorkflowGuidance>> {
let ctx = StartupContext {
layout,
repo_root,
locality_id,
allow_cached_work,
};
collect_radar_workflow_guidance_from(®istered(), &ctx)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn radar_behavioral_drift_signals(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
allow_cached_work: bool,
git: Option<&GitState>,
handoff_title: &str,
handoff_immediate_actions: &[String],
active_session_id: Option<&str>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
let ctx = RadarContext {
startup: StartupContext {
layout,
repo_root,
locality_id,
allow_cached_work,
},
git,
handoff_title,
handoff_immediate_actions,
active_session_id,
};
collect_radar_behavioral_drift_signals_from(®istered(), &ctx)
}
fn collect_radar_workflow_guidance_from(
extensions: &[&dyn Extension],
ctx: &StartupContext<'_>,
) -> Result<Option<RadarWorkflowGuidance>> {
let mut guidance = None;
let mut owner = None;
for extension in extensions {
if let Some(candidate) = extension.radar_workflow_guidance(ctx)? {
if let Some(existing_owner) = owner {
bail!(
"multiple extensions (`{existing_owner}` and `{}`) contributed radar workflow guidance; exactly one guidance contributor is supported",
extension.name()
);
}
owner = Some(extension.name());
guidance = Some(candidate);
}
}
Ok(guidance)
}
fn collect_radar_behavioral_drift_signals_from(
extensions: &[&dyn Extension],
ctx: &RadarContext<'_>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
let mut seen = BTreeSet::new();
let mut signals = Vec::new();
for extension in extensions {
for signal in extension.radar_behavioral_drift_signals(ctx)? {
if !seen.insert(signal.id) {
bail!(
"multiple extensions contributed radar behavioral drift signal `{}`; signal ids must be unique",
signal.id
);
}
signals.push(signal);
}
}
Ok(signals)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::ProfileName;
struct TestExtension {
name: &'static str,
workflow_guidance: Option<&'static str>,
behavioral_drift_signal: Option<&'static str>,
}
impl Extension for TestExtension {
fn name(&self) -> &'static str {
self.name
}
fn command_groups(&self) -> &'static [&'static str] {
&["test"]
}
fn radar_workflow_guidance(
&self,
_ctx: &StartupContext<'_>,
) -> Result<Option<RadarWorkflowGuidance>> {
Ok(self.workflow_guidance.map(|summary| RadarWorkflowGuidance {
evaluation: RadarEvaluationBucket {
status: "loaded",
summary: summary.to_owned(),
evidence: Vec::new(),
},
approval_steps: Vec::new(),
}))
}
fn radar_behavioral_drift_signals(
&self,
_ctx: &RadarContext<'_>,
) -> Result<Vec<RadarBehavioralDriftSignal>> {
Ok(self
.behavioral_drift_signal
.map(|id| {
vec![RadarBehavioralDriftSignal {
id,
status: RadarBehavioralDriftStatus::NoSignal,
summary: "test".to_owned(),
evidence: Vec::new(),
recommended_correction: None,
}]
})
.unwrap_or_default())
}
}
#[test]
fn owned_command_groups_reflect_registered_extensions() {
let groups = owned_command_groups();
#[cfg(feature = "extension-backlog")]
assert!(groups.contains(&"backlog"));
#[cfg(not(feature = "extension-backlog"))]
assert!(!groups.contains(&"backlog"));
#[cfg(feature = "extension-codemap")]
assert!(groups.contains(&"codemap"));
#[cfg(not(feature = "extension-codemap"))]
assert!(!groups.contains(&"codemap"));
}
#[test]
fn extension_mcp_tools_reflect_registered_extensions() {
let schema = crate::commands::describe::run();
let tools = build_mcp_tools(&schema.commands);
let names = tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>();
#[cfg(feature = "extension-backlog")]
assert!(names.contains(&"ccd_backlog"));
#[cfg(not(feature = "extension-backlog"))]
assert!(!names.contains(&"ccd_backlog"));
#[cfg(feature = "extension-codemap")]
assert!(names.contains(&"ccd_codemap"));
#[cfg(not(feature = "extension-codemap"))]
assert!(!names.contains(&"ccd_codemap"));
}
#[test]
fn extension_cli_commands_reflect_registered_extensions() {
let command = augment_clap(clap::Command::new("ccd"));
let names = command
.get_subcommands()
.map(|command| command.get_name())
.collect::<Vec<_>>();
#[cfg(feature = "extension-backlog")]
assert!(names.contains(&"backlog"));
#[cfg(not(feature = "extension-backlog"))]
assert!(!names.contains(&"backlog"));
#[cfg(feature = "extension-codemap")]
assert!(names.contains(&"codemap"));
#[cfg(not(feature = "extension-codemap"))]
assert!(!names.contains(&"codemap"));
}
#[test]
fn radar_workflow_guidance_is_not_limited_to_dispatch_owner() {
let extension = TestExtension {
name: "observer",
workflow_guidance: Some("generic workflow guidance"),
behavioral_drift_signal: None,
};
let extensions: [&dyn Extension; 1] = [&extension];
let layout = StateLayout::new(
"/tmp/home/.ccd".into(),
"/tmp/repo/.git/ccd".into(),
ProfileName::new("main").expect("profile"),
);
let ctx = StartupContext {
layout: &layout,
repo_root: Path::new("/tmp/repo"),
locality_id: "ccdrepo_test",
allow_cached_work: true,
};
let guidance =
collect_radar_workflow_guidance_from(&extensions, &ctx).expect("workflow guidance");
assert_eq!(
guidance.expect("guidance present").evaluation.summary,
"generic workflow guidance"
);
}
#[test]
fn radar_behavioral_drift_signals_are_not_limited_to_dispatch_owner() {
let extension = TestExtension {
name: "observer",
workflow_guidance: None,
behavioral_drift_signal: Some("observer_signal"),
};
let extensions: [&dyn Extension; 1] = [&extension];
let layout = StateLayout::new(
"/tmp/home/.ccd".into(),
"/tmp/repo/.git/ccd".into(),
ProfileName::new("main").expect("profile"),
);
let startup = StartupContext {
layout: &layout,
repo_root: Path::new("/tmp/repo"),
locality_id: "ccdrepo_test",
allow_cached_work: true,
};
let git = GitState {
branch: "main".to_owned(),
head: "deadbee".to_owned(),
upstream: None,
ahead: 0,
behind: 0,
clean: true,
staged_files: Vec::new(),
unstaged_files: Vec::new(),
untracked_files: Vec::new(),
recent_commits: Vec::new(),
};
let ctx = RadarContext {
startup,
git: Some(&git),
handoff_title: "Next Session: Test",
handoff_immediate_actions: &[],
active_session_id: None,
};
let signals = collect_radar_behavioral_drift_signals_from(&extensions, &ctx)
.expect("behavioral drift signals");
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].id, "observer_signal");
assert_eq!(signals[0].status, RadarBehavioralDriftStatus::NoSignal);
}
}