Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/tracedecay-dashboard-api/src/events_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,7 @@ pub(crate) async fn dashboard_state_fixture(
code_index_freshness_reader: None,
explorer_semantic_reader: None,
feedback_status_reader: None,
pr_autotrack_reader: None,
storage_mode: "profile_sharded".to_owned(),
store_root,
config_path: project.path().join("config.json"),
Expand Down
16 changes: 13 additions & 3 deletions crates/tracedecay-dashboard-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,8 @@ mod settings_api;
pub use settings_api::{
DashboardCodeIndexWorkerConfigurationV1, DashboardCodeIndexWorkerSettingsCommitFuture,
DashboardCodeIndexWorkerSettingsCommitV1, DashboardCodeIndexWorkerSettingsErrorV1,
DashboardCodeIndexWorkerSettingsFuture, DashboardPrAutoTrackEntryV1,
DashboardPrAutoTrackReadPort, DashboardProfileCodeIndexWorkerSettingsPort,
install_dashboard_pr_autotrack_read_port,
DashboardCodeIndexWorkerSettingsFuture, DashboardProfileCodeIndexWorkerSettingsPort,
PrAutoTrackManagedSummaryEntryV1, PrAutoTrackManagedSummaryReader,
};
mod storage_findings_api;
mod storage_telemetry_api;
Expand Down Expand Up @@ -351,6 +350,10 @@ pub struct DashboardStateCompositionV1 {
/// it absent and the source reports typed `unsupported`.
pub explorer_semantic_reader: Option<ExplorerSemanticReader>,
pub feedback_status_reader: Option<feedback_api::FeedbackStatusReader>,
/// Root-addressed read over the daemon-owned PR-autotrack state sidecar.
/// Selected projects reuse the resolver but resolve their own exact store
/// root on every call.
pub pr_autotrack_reader: Option<settings_api::PrAutoTrackManagedSummaryReader>,
pub code_diagnostics_broker:
Option<Arc<tokio::sync::Mutex<tracedecay_lsp::analyzer::broker::DiagnosticBroker>>>,
pub application_invocation_executor: Option<Arc<dyn DashboardApplicationRuntime>>,
Expand Down Expand Up @@ -463,6 +466,8 @@ pub struct DashboardState {
/// observation owner. Selected projects reuse the resolver but resolve
/// their own exact project root on every call.
pub feedback_status_reader: Option<feedback_api::FeedbackStatusReader>,
/// Daemon-owned read over PR-autotrack managed branches for settings.
pub pr_autotrack_reader: Option<settings_api::PrAutoTrackManagedSummaryReader>,
/// Storage mode resolved for the active project store.
pub storage_mode: String,
/// Resolved active project store root.
Expand Down Expand Up @@ -776,6 +781,7 @@ async fn build_state_inner(
code_index_freshness_reader,
explorer_semantic_reader,
feedback_status_reader,
pr_autotrack_reader,
code_diagnostics_broker,
application_invocation_executor,
delivery_settlement_authority,
Expand Down Expand Up @@ -848,6 +854,7 @@ async fn build_state_inner(
code_index_freshness_reader,
explorer_semantic_reader,
feedback_status_reader,
pr_autotrack_reader,
storage_mode,
store_root,
config_path,
Expand Down Expand Up @@ -933,6 +940,7 @@ pub async fn build_selected_project_state(
// resolves the selected state's exact root on every call.
explorer_semantic_reader: active.explorer_semantic_reader.clone(),
feedback_status_reader: active.feedback_status_reader.clone(),
pr_autotrack_reader: active.pr_autotrack_reader.clone(),
code_diagnostics_broker: None,
// Rebinding an application transport is required only for the
// selected project's application routes. Ordinary read routes
Expand Down Expand Up @@ -1077,6 +1085,7 @@ where
code_index_freshness_reader: None,
explorer_semantic_reader: None,
feedback_status_reader: None,
pr_autotrack_reader: None,
code_diagnostics_broker: Some(code_diagnostics_broker),
application_invocation_executor: test_authority
.and_then(|authority| authority.application_invocation_executor.clone()),
Expand Down Expand Up @@ -2391,6 +2400,7 @@ mod authority_tests {
code_index_freshness_reader: None,
explorer_semantic_reader: None,
feedback_status_reader: None,
pr_autotrack_reader: None,
storage_mode: storage_mode_label(&layout.storage_mode).to_owned(),
store_root: layout.data_root.clone(),
config_path: layout.config_path.clone(),
Expand Down
85 changes: 57 additions & 28 deletions crates/tracedecay-dashboard-api/src/settings_api.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Dashboard endpoints for project and user settings.

use std::future::Future;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use std::sync::Arc;

use axum::Json;
use axum::extract::State;
Expand Down Expand Up @@ -296,31 +296,24 @@ struct PrAutoTrackPayloadV1 {
tracked: Vec<PrAutoTrackEntryV1>,
}

#[derive(Clone, Debug, JsonSchema, Serialize)]
#[derive(Clone, Debug, PartialEq, JsonSchema, Serialize)]
struct PrAutoTrackEntryV1 {
branch: String,
pr: u64,
head_branch: String,
}

#[derive(Clone, Debug)]
pub struct DashboardPrAutoTrackEntryV1 {
/// One managed PR branch projected for dashboard settings payloads.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrAutoTrackManagedSummaryEntryV1 {
pub branch: String,
pub pr: u64,
pub head_branch: String,
}

pub trait DashboardPrAutoTrackReadPort: Send + Sync {
fn managed_summary(&self, store_root: &Path) -> Vec<DashboardPrAutoTrackEntryV1>;
}

static PR_AUTOTRACK_READ_PORT: OnceLock<Arc<dyn DashboardPrAutoTrackReadPort>> = OnceLock::new();

pub fn install_dashboard_pr_autotrack_read_port(
port: Arc<dyn DashboardPrAutoTrackReadPort>,
) -> Result<(), Arc<dyn DashboardPrAutoTrackReadPort>> {
PR_AUTOTRACK_READ_PORT.set(port)
}
/// Root-addressed read over the daemon-owned PR-autotrack state sidecar.
pub type PrAutoTrackManagedSummaryReader =
Arc<dyn Fn(PathBuf) -> Vec<PrAutoTrackManagedSummaryEntryV1> + Send + Sync + 'static>;

#[hotpath::measure(label = "dashboard_api.settings.get", future = true)]
pub async fn get_settings(State(state): State<DashboardState>) -> ApiResult {
Expand Down Expand Up @@ -732,19 +725,24 @@ fn automation_settings_payload(
/// Lists the PR branches the daemon currently auto-tracks for this project, read
/// from the store's PR-autotrack state sidecar. Empty on non-unix or when the
/// feature has tracked nothing yet.
fn pr_autotrack_payload(state: &DashboardState) -> PrAutoTrackPayloadV1 {
let tracked = PR_AUTOTRACK_READ_PORT
.get()
.map(|port| {
port.managed_summary(&state.store_root)
.into_iter()
.map(|entry| PrAutoTrackEntryV1 {
branch: entry.branch,
pr: entry.pr,
head_branch: entry.head_branch,
})
.collect()
fn map_managed_pr_autotrack_entries(
entries: Vec<PrAutoTrackManagedSummaryEntryV1>,
) -> Vec<PrAutoTrackEntryV1> {
entries
.into_iter()
.map(|entry| PrAutoTrackEntryV1 {
branch: entry.branch,
pr: entry.pr,
head_branch: entry.head_branch,
})
.collect()
}

fn pr_autotrack_payload(state: &DashboardState) -> PrAutoTrackPayloadV1 {
let tracked = state
.pr_autotrack_reader
.as_ref()
.map(|reader| map_managed_pr_autotrack_entries(reader(state.store_root.clone())))
.unwrap_or_default();
PrAutoTrackPayloadV1 { tracked }
}
Expand Down Expand Up @@ -914,6 +912,37 @@ mod tests {
);
}

#[test]
fn managed_pr_autotrack_projection_preserves_payload_fields() {
let mapped = map_managed_pr_autotrack_entries(vec![
PrAutoTrackManagedSummaryEntryV1 {
branch: "tracedecay/autotrack/pr/1".into(),
pr: 1,
head_branch: "alpha".into(),
},
PrAutoTrackManagedSummaryEntryV1 {
branch: "tracedecay/autotrack/pr/3".into(),
pr: 3,
head_branch: "beta".into(),
},
]);
assert_eq!(
mapped,
vec![
PrAutoTrackEntryV1 {
branch: "tracedecay/autotrack/pr/1".into(),
pr: 1,
head_branch: "alpha".into(),
},
PrAutoTrackEntryV1 {
branch: "tracedecay/autotrack/pr/3".into(),
pr: 3,
head_branch: "beta".into(),
},
]
);
}

#[test]
fn exact_workers_above_current_memory_safe_limit_are_a_typed_refusal() {
let status = CodeIndexWorkerStatusV1 {
Expand Down
31 changes: 3 additions & 28 deletions crates/tracedecay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,41 +928,16 @@ impl tracedecay_configuration::config::PinnedRuntimeConfigurationCachePort
}
}

/// Dashboard read port over the daemon's PR-autotrack state sidecar.
struct DaemonPrAutoTrackReadPort;

impl tracedecay_dashboard_api::DashboardPrAutoTrackReadPort for DaemonPrAutoTrackReadPort {
fn managed_summary(
&self,
store_root: &Path,
) -> Vec<tracedecay_dashboard_api::DashboardPrAutoTrackEntryV1> {
crate::daemon::pr_autotrack::managed_summary(store_root)
.into_iter()
.map(
|entry| tracedecay_dashboard_api::DashboardPrAutoTrackEntryV1 {
branch: entry.branch,
pr: entry.pr,
head_branch: entry.head_branch,
},
)
.collect()
}
}

/// Installs the root-owned configuration read ports the lower crates reach
/// through their process-global slots: the pin cache, the dashboard
/// configuration reader, and the PR-autotrack reader. Idempotent.
/// through their process-global slots: the pin cache and the dashboard
/// configuration reader. Idempotent.
pub(crate) fn install_usecase_runtime_configuration_authority() -> Result<()> {
static INSTALLATION: LazyLock<std::result::Result<(), String>> = LazyLock::new(|| {
tracedecay_configuration::config::install_pinned_runtime_configuration_cache(Arc::new(
RootPinnedRuntimeConfigurationCache,
))
.map_err(|error| error.to_string())?;
install_dashboard_configuration_read_port().map_err(|error| error.to_string())?;
tracedecay_dashboard_api::install_dashboard_pr_autotrack_read_port(Arc::new(
DaemonPrAutoTrackReadPort,
))
.map_err(|_| "dashboard PR autotrack read port was already installed".to_string())
install_dashboard_configuration_read_port().map_err(|error| error.to_string())
});
INSTALLATION
.as_ref()
Expand Down
84 changes: 84 additions & 0 deletions crates/tracedecay/src/daemon/pr_autotrack/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1349,3 +1349,87 @@ async fn cancelled_activation_keeps_its_lifecycle_owner_bounded_during_stalled_e
drop(lifecycle);
schedulers.shutdown().await;
}

#[test]
fn dashboard_managed_summary_reader_matches_canonical_state() {
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;

let data_root = tempfile::tempdir().expect("temp data root");
let state = PrAutotrackState {
managed: BTreeMap::from([
(
pr_label(3),
ManagedPr {
pr: 3,
head_branch: "feature-three".into(),
head_sha: String::new(),
worktree: PathBuf::from("/tmp/pr-3"),
tracking_ref: pr_tracking_ref(3),
},
),
(
pr_label(1),
ManagedPr {
pr: 1,
head_branch: "feature-one".into(),
head_sha: String::new(),
worktree: PathBuf::from("/tmp/pr-1"),
tracking_ref: pr_tracking_ref(1),
},
),
]),
};
std::fs::write(
state_path(data_root.path()),
serde_json::to_string_pretty(&state).expect("serialize pr-autotrack state"),
)
.expect("write pr-autotrack state");

let canonical = managed_summary(data_root.path());
let reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader =
Arc::new(|store_root| {
managed_summary(&store_root)
.into_iter()
.map(
|entry| tracedecay_dashboard_api::PrAutoTrackManagedSummaryEntryV1 {
branch: entry.branch,
pr: entry.pr,
head_branch: entry.head_branch,
},
)
.collect()
});
let projected = reader(data_root.path().to_path_buf());

assert_eq!(projected.len(), canonical.len());
for (entry, summary) in projected.iter().zip(canonical.iter()) {
assert_eq!(entry.branch, summary.branch);
assert_eq!(entry.pr, summary.pr);
assert_eq!(entry.head_branch, summary.head_branch);
}
assert_eq!(projected[0].pr, 1);
assert_eq!(projected[1].pr, 3);
}

#[test]
fn dashboard_managed_summary_reader_is_empty_without_state() {
use std::sync::Arc;

let data_root = tempfile::tempdir().expect("temp data root");
let reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader =
Arc::new(|store_root| {
managed_summary(&store_root)
.into_iter()
.map(
|entry| tracedecay_dashboard_api::PrAutoTrackManagedSummaryEntryV1 {
branch: entry.branch,
pr: entry.pr,
head_branch: entry.head_branch,
},
)
.collect()
});
assert!(reader(data_root.path().to_path_buf()).is_empty());
}
19 changes: 19 additions & 0 deletions crates/tracedecay/src/daemon/project_composition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ struct ProjectRoutePorts {
tracedecay_dashboard_api::code_index_freshness_api::CodeIndexFreshnessReader,
dashboard_explorer_semantic_reader: tracedecay_dashboard_api::ExplorerSemanticReader,
dashboard_feedback_status_reader: tracedecay_dashboard_api::feedback_api::FeedbackStatusReader,
dashboard_pr_autotrack_reader: tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader,
diagnostic_broker: Arc<tokio::sync::Mutex<tracedecay_lsp::analyzer::broker::DiagnosticBroker>>,
code_index_hook_sink: crate::mcp::server::CodeIndexHookSink,
code_index_reconcile_sink: crate::mcp::server::CodeIndexReconcileSink,
Expand Down Expand Up @@ -493,6 +494,7 @@ impl ComposedCoreServer {
.with_dashboard_feedback_status_reader(Arc::clone(
&ports.dashboard_feedback_status_reader,
))
.with_dashboard_pr_autotrack_reader(Arc::clone(&ports.dashboard_pr_autotrack_reader))
.with_diagnostics_lsp(Arc::clone(&ports.diagnostic_broker))
.with_code_index_hook_sink(Arc::clone(&ports.code_index_hook_sink))
.with_code_index_reconcile_sink(Arc::clone(&ports.code_index_reconcile_sink))
Expand Down Expand Up @@ -884,6 +886,7 @@ impl ProjectOpenInputs<'_> {
tracedecay_dashboard_api::feedback_api::feedback_status_reader(
self.invocation.feedback_runtime_registrar(),
),
dashboard_pr_autotrack_reader: project_dashboard_pr_autotrack_reader(),
diagnostic_broker,
code_index_hook_sink,
code_index_reconcile_sink,
Expand Down Expand Up @@ -1942,6 +1945,22 @@ fn project_dashboard_freshness_reader(
reader
}

fn project_dashboard_pr_autotrack_reader()
-> tracedecay_dashboard_api::PrAutoTrackManagedSummaryReader {
Arc::new(|store_root| {
crate::daemon::pr_autotrack::managed_summary(&store_root)
.into_iter()
.map(
|entry| tracedecay_dashboard_api::PrAutoTrackManagedSummaryEntryV1 {
branch: entry.branch,
pr: entry.pr,
head_branch: entry.head_branch,
},
)
.collect()
})
}

/// Register the project graph and the session databases this route owns with
/// the sampling authority. An unavailable registration is recorded and skipped,
/// never fatal: telemetry must not fail an otherwise healthy project open.
Expand Down
Loading
Loading