use serde::Serialize;
pub const ATP_USER_JOURNEY_CONTRACT_VERSION: &str = "atp-user-journey-e2e.v1";
pub const ATP_USER_JOURNEY_REQUIRED_LOG_FIELDS: &[&str] = &[
"event",
"scenario_id",
"command_line",
"config_profile",
"daemon_ids",
"transfer_id",
"path_summary",
"manifest_root",
"receive_plan_digest",
"progress_event",
"quarantine_path_state",
"final_path_state",
"final_proof",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct AtpUserJourneyScenario {
pub scenario_id: &'static str,
pub journey: &'static str,
pub surfaces: &'static [&'static str],
pub command_lines: &'static [&'static str],
pub sdk_apis: &'static [&'static str],
pub daemon_events: &'static [&'static str],
pub receive_policy: &'static [&'static str],
pub path_modes: &'static [&'static str],
pub artifact_paths: &'static [&'static str],
pub human_output_assertions: &'static [&'static str],
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AtpUserJourneyContract {
pub contract_version: &'static str,
pub required_log_fields: &'static [&'static str],
pub scenarios: &'static [AtpUserJourneyScenario],
}
const SCENARIOS: &[AtpUserJourneyScenario] = &[
AtpUserJourneyScenario {
scenario_id: "first_pairing_share_code",
journey: "first pairing and share code",
surfaces: &["cli", "atpd", "sdk"],
command_lines: &[
"asupersync atp serve --profile first-run --json",
"asupersync atp share ./fixture --peer bob --json",
"asupersync atp status --json",
],
sdk_apis: &["AtpSdk::create_identity", "AtpSdk::create_share_code"],
daemon_events: &["daemon_start", "identity_created", "grant_recorded"],
receive_policy: &["deny_by_default"],
path_modes: &["direct"],
artifact_paths: &[
"structured_events.jsonl",
"identity_bundle.json",
"grant_log.jsonl",
],
human_output_assertions: &["concise_pairing_code", "no_secret_echo"],
},
AtpUserJourneyScenario {
scenario_id: "send_receive_explicit_approval",
journey: "send and receive with explicit approval",
surfaces: &["cli", "atpd", "sdk"],
command_lines: &[
"asupersync atp send ./fixture bob --name demo --json",
"asupersync atp inbox --json",
"asupersync atp get transfer-demo ./out --approve --json",
],
sdk_apis: &[
"AtpSdk::send",
"AtpSdk::receive_plan",
"ReceivePlan::approve",
],
daemon_events: &[
"sender_daemon_start",
"receiver_daemon_start",
"inbox_insert",
],
receive_policy: &["explicit_approval", "safe_destination"],
path_modes: &["direct", "relay"],
artifact_paths: &["manifest.json", "receive_plan.json", "proof_bundle.json"],
human_output_assertions: &["short_progress", "stable_json"],
},
AtpUserJourneyScenario {
scenario_id: "receive_safety_deny_quarantine",
journey: "receive safety, deny, quarantine, and dry-run",
surfaces: &["cli", "atpd", "sdk"],
command_lines: &[
"asupersync atp get transfer-demo ./out --dry-run --json",
"asupersync atp get transfer-demo ./out --deny --json",
"asupersync atp get transfer-demo ./out --quarantine-only --json",
],
sdk_apis: &[
"AtpSdk::receive_plan",
"ReceivePlan::deny",
"ReceivePlan::quarantine_only",
],
daemon_events: &[
"receive_plan_constructed",
"receive_denied",
"quarantine_created",
],
receive_policy: &[
"deny_by_default",
"quarantine_only",
"safe_destination",
"dry_run",
],
path_modes: &["mailbox"],
artifact_paths: &[
"receive_plan.json",
"quarantine_manifest.json",
"safety_report.json",
],
human_output_assertions: &[
"danger_explained_before_execution",
"no_mutation_on_dry_run",
],
},
AtpUserJourneyScenario {
scenario_id: "sync_mirror_watch_seed",
journey: "sync, mirror, watch, and seed",
surfaces: &["cli", "sdk"],
command_lines: &[
"asupersync atp sync ./left bob:/right --json",
"asupersync atp mirror ./left bob:/mirror --json",
"asupersync atp watch ./left bob:/watch --json",
"asupersync atp seed transfer-demo --json",
],
sdk_apis: &[
"AtpSdk::sync",
"AtpSdk::mirror",
"AtpSdk::watch",
"AtpSdk::seed",
],
daemon_events: &["watch_started", "seed_registered"],
receive_policy: &["policy_driven_auto_accept", "policy_driven_auto_deny"],
path_modes: &["direct", "relay"],
artifact_paths: &["sync_plan.json", "mirror_plan.json", "watch_events.jsonl"],
human_output_assertions: &["concise_conflict_summary", "stable_json"],
},
AtpUserJourneyScenario {
scenario_id: "resume_cancel_restart",
journey: "resume, cancel, shutdown, and daemon restart",
surfaces: &["cli", "atpd", "sdk"],
command_lines: &[
"asupersync atp cancel transfer-demo --json",
"asupersync atp resume transfer-demo --json",
"asupersync atp status transfer-demo --json",
],
sdk_apis: &["AtpSdk::cancel", "AtpSdk::resume", "AtpSdk::status"],
daemon_events: &["shutdown_requested", "daemon_restart", "journal_recovered"],
receive_policy: &["resume_preserves_receive_plan"],
path_modes: &["direct"],
artifact_paths: &[
"resume_journal.jsonl",
"restart_report.json",
"final_proof.json",
],
human_output_assertions: &["interruption_recovery_explained", "stable_json"],
},
AtpUserJourneyScenario {
scenario_id: "nat_tailscale_relay_mailbox",
journey: "NAT fallback, optional Tailscale, relay, and mailbox",
surfaces: &["cli", "atpd"],
command_lines: &[
"asupersync atp send ./fixture bob --path auto --json",
"asupersync atp status transfer-demo --explain --json",
],
sdk_apis: &["AtpSdk::path_candidates", "AtpSdk::mailbox_enqueue"],
daemon_events: &[
"nat_probe",
"tailscale_candidate_optional",
"relay_fallback",
],
receive_policy: &["mailbox_requires_receive_plan"],
path_modes: &["nat_fallback", "tailscale_optional", "relay", "mailbox"],
artifact_paths: &[
"path_graph.json",
"relay_trace.jsonl",
"mailbox_receipt.json",
],
human_output_assertions: &["path_reason_visible", "stable_json"],
},
AtpUserJourneyScenario {
scenario_id: "doctor_trace_replay_bench",
journey: "doctor, trace, replay, and bench smoke",
surfaces: &["cli"],
command_lines: &[
"asupersync atp doctor --json",
"asupersync trace verify trace.bin --json",
"asupersync lab replay scenario.yaml --json",
"asupersync atp bench --smoke --json",
],
sdk_apis: &["AtpSdk::doctor", "AtpSdk::bench_smoke"],
daemon_events: &["diagnostics_collected"],
receive_policy: &["not_applicable"],
path_modes: &["loopback"],
artifact_paths: &["doctor.json", "trace.json", "replay.json", "bench.json"],
human_output_assertions: &["human_output_concise", "stable_json"],
},
AtpUserJourneyScenario {
scenario_id: "proof_verify_failure_discovery",
journey: "proof, verify, and failure artifact discovery",
surfaces: &["cli", "sdk"],
command_lines: &[
"asupersync atp proof proof_bundle.json --summary --json",
"asupersync atp verify proof_bundle.json --strict --json",
],
sdk_apis: &["AtpSdk::proof", "AtpSdk::verify"],
daemon_events: &["proof_recorded"],
receive_policy: &["proof_bound_to_receive_plan"],
path_modes: &["direct", "relay", "mailbox"],
artifact_paths: &[
"proof_bundle.json",
"verification_report.json",
"failure_index.json",
],
human_output_assertions: &["proof_artifacts_discoverable_after_failure", "stable_json"],
},
];
#[must_use]
pub const fn atp_user_journey_scenarios() -> &'static [AtpUserJourneyScenario] {
SCENARIOS
}
#[must_use]
pub const fn atp_user_journey_required_log_fields() -> &'static [&'static str] {
ATP_USER_JOURNEY_REQUIRED_LOG_FIELDS
}
#[must_use]
pub const fn atp_user_journey_contract() -> AtpUserJourneyContract {
AtpUserJourneyContract {
contract_version: ATP_USER_JOURNEY_CONTRACT_VERSION,
required_log_fields: ATP_USER_JOURNEY_REQUIRED_LOG_FIELDS,
scenarios: SCENARIOS,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn values_for<F>(extract: F) -> BTreeSet<&'static str>
where
F: Fn(&AtpUserJourneyScenario) -> &'static [&'static str],
{
SCENARIOS
.iter()
.flat_map(extract)
.copied()
.collect::<BTreeSet<_>>()
}
#[test]
fn user_journey_matrix_covers_cli_daemon_sdk_and_safety_paths() {
let surfaces = values_for(|scenario| scenario.surfaces);
for expected in ["cli", "atpd", "sdk"] {
assert!(surfaces.contains(expected), "missing surface {expected}");
}
let policies = values_for(|scenario| scenario.receive_policy);
for expected in [
"deny_by_default",
"explicit_approval",
"quarantine_only",
"safe_destination",
"dry_run",
"policy_driven_auto_accept",
"policy_driven_auto_deny",
] {
assert!(
policies.contains(expected),
"missing receive policy {expected}"
);
}
let paths = values_for(|scenario| scenario.path_modes);
for expected in [
"direct",
"nat_fallback",
"tailscale_optional",
"relay",
"mailbox",
] {
assert!(paths.contains(expected), "missing path mode {expected}");
}
}
#[test]
fn user_journey_matrix_covers_core_operator_commands() {
let command_blob = SCENARIOS
.iter()
.flat_map(|scenario| scenario.command_lines.iter())
.copied()
.collect::<Vec<_>>()
.join("\n");
for expected in [
"send", "get", "sync", "mirror", "share", "watch", "seed", "inbox", "status", "resume",
"cancel", "verify", "proof", "doctor", "trace", "replay", "bench",
] {
assert!(
command_blob.contains(expected),
"missing command coverage for {expected}"
);
}
}
#[test]
fn user_journey_log_contract_has_required_bundle_fields() {
let fields = ATP_USER_JOURNEY_REQUIRED_LOG_FIELDS
.iter()
.copied()
.collect::<BTreeSet<_>>();
for expected in [
"command_line",
"config_profile",
"daemon_ids",
"transfer_id",
"path_summary",
"manifest_root",
"receive_plan_digest",
"progress_event",
"quarantine_path_state",
"final_path_state",
"final_proof",
] {
assert!(fields.contains(expected), "missing log field {expected}");
}
let assertions = values_for(|scenario| scenario.human_output_assertions);
for expected in [
"danger_explained_before_execution",
"proof_artifacts_discoverable_after_failure",
"stable_json",
] {
assert!(
assertions.contains(expected),
"missing output assertion {expected}"
);
}
}
#[test]
fn user_journey_contract_serializes_with_stable_schema() {
let value = serde_json::to_value(atp_user_journey_contract())
.expect("user journey contract should serialize");
assert_eq!(value["contract_version"], ATP_USER_JOURNEY_CONTRACT_VERSION);
assert_eq!(
value["required_log_fields"]
.as_array()
.expect("required fields should serialize as an array")
.len(),
ATP_USER_JOURNEY_REQUIRED_LOG_FIELDS.len()
);
assert_eq!(
value["scenarios"]
.as_array()
.expect("scenarios should serialize as an array")
.len(),
SCENARIOS.len()
);
}
}