#[allow(clippy::wildcard_imports)]
use super::*;
#[test]
fn target_parsing_accepts_only_the_two_contract_targets() {
assert_eq!(Tool::from_target("gitlab").unwrap(), Tool::GitLab);
assert_eq!(Tool::from_target("linear").unwrap(), Tool::Linear);
assert!(Tool::from_target("github").is_err());
assert_eq!(Tool::GitLab.token_var(), "GITLAB_TOKEN");
assert_eq!(Tool::Linear.token_var(), "LINEAR_TOKEN");
assert_eq!(Tool::GitLab.binary(), "glab");
assert_eq!(Tool::Linear.binary(), "linear-cli");
}
#[test]
fn gitlab_status_mapping() {
let config = Config::default();
assert_eq!(
local_to_remote_status(Tool::GitLab, "todo", &config),
"opened"
);
assert_eq!(
local_to_remote_status(Tool::GitLab, "done", &config),
"closed"
);
assert_eq!(
remote_to_local_status(Tool::GitLab, "opened", &config),
"todo"
);
assert_eq!(
remote_to_local_status(Tool::GitLab, "closed", &config),
"done"
);
assert_eq!(
remote_to_local_status(Tool::GitLab, "anything-else", &config),
"todo"
);
}
#[test]
fn linear_status_mapping() {
let config = Config::default();
assert_eq!(
local_to_remote_status(Tool::Linear, "todo", &config),
"todo"
);
assert_eq!(
local_to_remote_status(Tool::Linear, "in review", &config),
"todo"
);
assert_eq!(
local_to_remote_status(Tool::Linear, "done", &config),
"Done"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "In Progress", &config),
"in progress"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "Done", &config),
"done"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "Unknown", &config),
"todo"
);
}
#[test]
fn days_from_civil_roundtrips_civil_from_days() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(days_from_civil(2000, 1, 1), 10957);
assert_eq!(days_from_civil(1970, 1, 2), 1);
assert_eq!(days_from_civil(2026, 5, 11), 20584);
}
#[test]
fn date_to_epoch_days_parses_only_yyyymmdd() {
assert_eq!(date_to_epoch_days("1970-01-01"), Some(0));
assert_eq!(date_to_epoch_days("2026-05-11"), Some(20_584));
assert_eq!(date_to_epoch_days("not-a-date"), None);
assert_eq!(date_to_epoch_days("2026-13-01"), None);
assert_eq!(date_to_epoch_days("2026-05-11T10:00:00"), None);
}
#[test]
fn iso_to_epoch_secs_accepts_contract_timestamp_shapes() {
let day = days_from_civil(2026, 5, 11) * 86_400;
assert_eq!(iso_to_epoch_secs("2026-05-11T00:00:00Z"), Some(day));
assert_eq!(iso_to_epoch_secs("2026-05-11 00:00:00"), Some(day));
assert_eq!(iso_to_epoch_secs("2026-05-11T00:00:00.123Z"), Some(day));
assert_eq!(
iso_to_epoch_secs("2026-05-11T00:00:00+02:00"),
Some(day - 7200)
);
assert_eq!(
iso_to_epoch_secs("2026-05-11T00:00:00-05:30"),
Some(day + 19_800)
);
assert_eq!(
iso_to_epoch_secs("2026-05-11T10:30:00Z"),
Some(day + 37_800)
);
assert_eq!(iso_to_epoch_secs("garbage"), None);
}
#[test]
fn remote_newer_wins_only_when_strictly_after_local_end_of_day() {
assert!(is_remote_newer("2026-05-12T00:00:00Z", "2026-05-11"));
assert!(!is_remote_newer("2026-05-11 13:00:00", "2026-05-11"));
assert!(!is_remote_newer("2026-05-11T23:59:59Z", "2026-05-11"));
assert!(!is_remote_newer("2026-05-10T23:59:59Z", "2026-05-11"));
assert!(!is_remote_newer("not-a-time", "2026-05-11"));
}
#[test]
fn scrub_replaces_secret_everywhere_and_preserves_text() {
assert_eq!(
scrub_secret("token=sekrit end", "sekrit"),
"token=<token> end"
);
assert_eq!(
scrub_secret("token=sekrit token=sekrit", "sekrit"),
"token=<token> token=<token>"
);
assert_eq!(scrub_secret("no secret here", "sekrit"), "no secret here");
assert_eq!(scrub_secret("empty secret", ""), "empty secret");
}
fn rel(kind: crate::relationships::RelationKind) -> crate::relationships::Relationship {
let mut r = crate::relationships::Relationship::new(1, 2, kind, "Ship it");
r.id = "rel_TEST".to_string();
r
}
#[test]
fn every_relationship_maps_only_where_the_wire_is_verified_and_otherwise_diagnoses() {
for tool in [Tool::GitLab, Tool::Linear] {
for kind in [
crate::relationships::RelationKind::Blocks,
crate::relationships::RelationKind::BlockedBy,
crate::relationships::RelationKind::Related,
crate::relationships::RelationKind::Duplicates,
] {
let model_supports = matches!(
kind,
crate::relationships::RelationKind::Blocks
| crate::relationships::RelationKind::BlockedBy
);
match map_relation_to_remote(tool, &rel(kind), Some("ENG-42")) {
MappedRelation::Remote { field, value } => {
assert!(
model_supports && RELATION_WIRE_VERIFIED.contains(&tool),
"{tool}/{kind} mapped, so the tool must model the kind and \
be on the verified wire"
);
assert_ne!(field, "");
if tool == Tool::Linear {
assert_eq!(
value, "ENG-42",
"Linear sends the target's remote identifier"
);
} else {
assert!(value.contains('#'));
}
}
MappedRelation::Unsupported(d) => {
assert!(
!RELATION_WIRE_VERIFIED.contains(&tool) || !model_supports,
"{tool}/{kind} is on the verified wire and modelled, so it \
must map, not diagnose"
);
assert_eq!(d.tool, tool, "the diagnostic names the tool");
assert_eq!(d.relation_id, "rel_TEST");
assert!(
!d.field.is_empty(),
"the diagnostic must name the specific field: {d}"
);
assert!(d.to_string().contains(&d.field), "and print it: {d}");
if model_supports {
assert!(
RELATION_CLI_ABSENT.contains(&tool)
&& d.message.contains("exposes no issue-relation command")
&& !d.message.contains("not verified"),
"{tool}/{kind}: a model-supported kind on an unverified \
tool must name the real reason; a CLI verified to have \
no relation command must say so, not 'not verified': {d}"
);
} else {
assert!(
!d.message.contains("not verified"),
"{tool}/{kind}: a kind with no remote field must not \
blame an unverified verb: {d}"
);
}
}
}
}
}
}
#[test]
fn a_relation_without_a_remote_target_is_reported_not_guessed() {
let MappedRelation::Unsupported(d) = map_relation_to_remote(
Tool::Linear,
&rel(crate::relationships::RelationKind::Blocks),
None,
) else {
panic!("a missing target must not map");
};
assert_eq!(d.tool, Tool::Linear);
assert_eq!(d.field, "blocks");
assert!(
d.message.contains("#2") && d.message.contains("not been exported"),
"the diagnostic names the target and why: {d}"
);
}
#[test]
fn the_reverse_direction_maps_exactly_when_the_wire_is_verified() {
for tool in [Tool::GitLab, Tool::Linear] {
let field = match tool {
Tool::GitLab => "blocking",
Tool::Linear => "blocks",
};
match map_relation_from_remote(tool, field, "blocks #2", "rel_TEST") {
MappedRelation::Remote { .. } => assert!(
RELATION_WIRE_VERIFIED.contains(&tool),
"{tool}: the reverse direction mapped, so the wire must be verified"
),
MappedRelation::Unsupported(d) => {
assert!(
!RELATION_WIRE_VERIFIED.contains(&tool),
"{tool}: the wire is verified, so the reverse direction must map"
);
assert_eq!(d.tool, tool);
assert_ne!(d.relation_id, "");
}
}
}
}
#[test]
fn an_unrecognised_remote_relation_field_never_becomes_a_local_edge() {
for tool in [Tool::GitLab, Tool::Linear] {
for (field, value) in [
("parent", "#2"),
("blocks", "not-a-number"),
("whatever", ""),
] {
match map_relation_from_remote(tool, field, value, "rel_X") {
MappedRelation::Remote { .. } => {
panic!("{tool}: `{field}={value}` must not become a local edge")
}
MappedRelation::Unsupported(d) => {
assert_eq!(d.tool, tool);
assert_eq!(d.field, field, "the diagnostic names the field");
}
}
}
}
}
#[test]
fn both_trackers_share_one_mapping_seam() {
let source = concat!(include_str!("../mod.rs"), include_str!("../adapters.rs"));
let fns = [
"pub fn map_relation_to_remote",
"pub fn map_relation_from_remote",
];
for f in fns {
assert_eq!(
source.matches(&format!("{f}(")).count(),
1,
"{f} must be *defined* exactly once, in this module, for both \
trackers — a doc comment naming it is not a second implementation"
);
}
for (name, text) in [
("glab.rs", include_str!("../glab.rs")),
("linear.rs", include_str!("../linear.rs")),
] {
assert!(
!text.contains("fn map_relation_to_remote")
&& !text.contains("fn map_relation_from_remote"),
"{name} must not define its own mapping seam; the shared one is \\
keyed on `Tool` precisely so the two cannot diverge"
);
}
}
#[test]
fn a_sync_report_carries_both_the_count_and_the_diagnostics() {
let mut report = SyncReport::default().diagnostics_in_report();
let mut sink: Vec<u8> = Vec::new();
report
.note(
map_relation_to_remote(
Tool::GitLab,
&rel(crate::relationships::RelationKind::Related),
None,
),
&mut sink,
)
.unwrap();
report
.note(
map_relation_to_remote(
Tool::GitLab,
&rel(crate::relationships::RelationKind::Blocks),
None,
),
&mut sink,
)
.unwrap();
let expected = if RELATION_WIRE_VERIFIED.contains(&Tool::GitLab) {
1
} else {
2
};
assert_eq!(
report.diagnostics.len(),
expected,
"the report records every relation the tool cannot carry"
);
assert_eq!(report.handled, 0, "no issues were handled here");
let text = String::from_utf8(sink).unwrap();
assert!(
text.contains("cannot represent"),
"the stream says so: {text}"
);
}
fn split_completion_config() -> Config {
Config {
kanban: crate::config::KanbanSection {
configurations: indexmap::IndexMap::from([(
"default".to_string(),
crate::config::BoardConfiguration {
columns: vec!["todo".into(), "shipped".into(), "done".into()],
done_column: Some("done".to_string()),
category_map: indexmap::IndexMap::from([
("todo".to_string(), "unstarted".to_string()),
("shipped".to_string(), "completed".to_string()),
("done".to_string(), "active".to_string()),
]),
..Default::default()
},
)]),
},
..Config::default()
}
}
#[test]
fn t032_a_remote_closed_state_maps_to_the_services_completion_column() {
let config = split_completion_config();
assert_eq!(
remote_to_local_status(Tool::GitLab, "closed", &config),
"shipped",
"a closed GitLab issue belongs in the column that is actually complete"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "Done", &config),
"shipped"
);
}
#[test]
fn t032_export_closes_the_column_the_service_calls_complete() {
let config = split_completion_config();
assert_eq!(
local_to_remote_status(Tool::GitLab, "shipped", &config),
"closed"
);
assert_eq!(
local_to_remote_status(Tool::GitLab, "done", &config),
"opened"
);
assert_eq!(
local_to_remote_status(Tool::Linear, "shipped", &config),
"Done"
);
}
#[test]
fn t032_a_closed_remote_state_mapped_to_a_canceled_column_is_not_complete() {
let mut config = split_completion_config();
config
.kanban
.configurations
.get_mut("default")
.unwrap()
.category_map
.insert("done".to_string(), "canceled".to_string());
let board = config.default_config();
assert_eq!(
local_to_remote_status(Tool::GitLab, "shipped", &config),
"closed"
);
assert!(
!is_done_column(board, "done", &config),
"a canceled column is not locally complete"
);
}
#[test]
fn t032_the_existing_round_trips_are_unchanged() {
let config = Config::default();
assert_eq!(
local_to_remote_status(Tool::GitLab, "done", &config),
"closed"
);
assert_eq!(
local_to_remote_status(Tool::GitLab, "todo", &config),
"opened"
);
assert_eq!(
remote_to_local_status(Tool::GitLab, "closed", &config),
"done"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "Done", &config),
"done"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "In Progress", &config),
"in progress",
"a name match wins over the first column"
);
assert_eq!(
remote_to_local_status(Tool::Linear, "No Such State", &config),
"todo",
"and with no match at all, the first column"
);
}
#[test]
fn t032_the_sync_seam_holds_no_local_completion_definition() {
let source = concat!(include_str!("../mod.rs"), include_str!("../adapters.rs"));
let forbidden = ["fn remote", "status_is_done"].concat();
assert!(
!source.contains(&forbidden),
"the dead second definition is removed and must not come back"
);
let uses_service = source.matches("WorkflowCategory::is_complete").count()
+ source.matches("completion_column_or_default").count();
assert!(
uses_service >= 2,
"both `is_done_column` and `completion_column` must ask the service, found \
{uses_service} uses"
);
}
#[test]
fn t035_a_project_association_maps_back_to_its_local_id() {
for tool in [Tool::GitLab, Tool::Linear] {
let remote = RemoteProject {
id: "p-1".to_string(),
name: Some("One".to_string()),
};
match map_project_from_remote(tool, &remote) {
MappedProject::Remote { field, value } => {
assert_eq!(field, crate::projects::PROJECTS_KEY);
assert_eq!(value, "p-1", "the id, never the remote's name");
}
MappedProject::Unsupported(d) => {
panic!("{tool}: a reported id must map back: {d}")
}
}
}
}
#[test]
fn t035_every_project_association_diagnoses_with_its_field_its_tool_and_its_reason() {
for tool in [Tool::GitLab, Tool::Linear] {
match map_project_to_remote(tool, 3, "p-7") {
MappedProject::Remote { field, value } => {
assert!(
PROJECT_WIRE_VERIFIED.contains(&tool),
"{tool}: an association mapped without a verified membership field"
);
assert_eq!(field, project_field(tool));
assert_eq!(value, "p-7");
}
MappedProject::Unsupported(d) => {
assert_eq!(d.tool, tool, "the diagnostic names the tool");
assert_eq!(d.field, project_field(tool), "and the field");
assert_eq!(
d.relation_id, "3->p-7",
"and the issue and the association it is about"
);
let text = d.to_string();
for needle in [d.field.as_str(), "p-7", "#3"] {
assert!(text.contains(needle), "`{needle}` missing from: {text}");
}
assert!(
!d.message.contains("not verified"),
"{tool}: there is no membership verb to verify: {text}"
);
}
}
}
assert!(project_absence_reason(Tool::GitLab).contains("--project"));
assert!(project_absence_reason(Tool::Linear).contains("one `project`"));
assert_ne!(
project_absence_reason(Tool::GitLab),
project_absence_reason(Tool::Linear)
);
}
#[test]
fn t035_an_association_with_no_id_is_diagnosed_and_kept() {
for tool in [Tool::GitLab, Tool::Linear] {
match map_project_to_remote(tool, 4, " ") {
MappedProject::Remote { .. } => panic!("{tool}: nothing to send"),
MappedProject::Unsupported(d) => {
assert_eq!(d.tool, tool);
assert_eq!(d.field, project_field(tool));
assert!(d.message.contains("no id"), "{}", d.message);
assert!(
d.message.contains("kept locally"),
"and says the entry survives: {}",
d.message
);
}
}
}
}
#[test]
fn t035_remote_project_reading_keeps_order_and_accepts_every_known_shape() {
let list: serde_json::Value =
serde_json::from_str(r#"["p-1", {"id": "p-2", "name": "Two"}, 3]"#).unwrap();
assert_eq!(
remote_projects(Some(&list)),
vec![
RemoteProject {
id: "p-1".into(),
name: None
},
RemoteProject {
id: "p-2".into(),
name: Some("Two".into())
},
RemoteProject {
id: "3".into(),
name: None
},
],
"the payload's order is the list's order"
);
for raw in [
r#"{"identifier": "p-9"}"#,
r#"{"project_id": "p-9"}"#,
r#""p-9""#,
] {
let value: serde_json::Value = serde_json::from_str(raw).unwrap();
assert_eq!(
remote_projects(Some(&value)),
vec![RemoteProject {
id: "p-9".into(),
name: None
}],
"{raw}"
);
}
let named: serde_json::Value = serde_json::from_str(r#"[{"name": "Unidentified"}]"#).unwrap();
assert_eq!(
remote_projects(Some(&named)),
vec![RemoteProject {
id: String::new(),
name: Some("Unidentified".into())
}]
);
assert_eq!(remote_projects(None), [] as [crate::sync::RemoteProject; 0]);
assert_eq!(
remote_projects(Some(&serde_json::Value::Null)),
[] as [crate::sync::RemoteProject; 0]
);
assert_eq!(
remote_projects(Some(&serde_json::json!([true, false]))),
[] as [crate::sync::RemoteProject; 0]
);
}
#[test]
fn t035_a_remote_project_with_no_id_is_diagnosed_never_guessed_at() {
for tool in [Tool::GitLab, Tool::Linear] {
let remote = RemoteProject {
id: " ".to_string(),
name: Some("Named but unidentified".to_string()),
};
match map_project_from_remote(tool, &remote) {
MappedProject::Remote { .. } => {
panic!("{tool}: an absent id cannot become a local association")
}
MappedProject::Unsupported(d) => {
assert_eq!(d.tool, tool);
assert_eq!(d.field, project_field(tool));
assert!(d.message.contains("Named but unidentified"));
assert!(d.message.contains("no id"));
}
}
}
}
#[test]
fn t036_a_project_diagnostic_built_from_remote_output_is_scrubbed() {
let mut diagnostic = RemoteMappingDiagnostic {
relation_id: "1->lin-secret".to_string(),
field: LINEAR_PROJECT_FIELD.to_string(),
tool: Tool::Linear,
message: "the remote reports #1 in project `p-1` (`lin-secret`)".to_string(),
};
scrub_diagnostic(&mut diagnostic, Some("lin-secret"));
assert!(
diagnostic.message.contains("<token>"),
"{}",
diagnostic.message
);
assert!(!diagnostic.message.contains("lin-secret"));
assert!(!diagnostic.relation_id.contains("lin-secret"));
let mut untouched = RemoteMappingDiagnostic {
relation_id: "1->p-1".to_string(),
field: LINEAR_PROJECT_FIELD.to_string(),
tool: Tool::Linear,
message: "no secret here".to_string(),
};
let expected = untouched.clone();
scrub_diagnostic(&mut untouched, None);
assert_eq!(untouched, expected);
}
#[test]
fn t036_no_tracker_sends_a_membership_and_both_adapters_share_the_seam() {
for tool in [Tool::GitLab, Tool::Linear] {
assert!(!PROJECT_WIRE_VERIFIED.contains(&tool));
assert!(matches!(
map_project_to_remote(tool, 1, "p-1"),
MappedProject::Unsupported(_)
));
}
let source = concat!(include_str!("../mod.rs"), include_str!("../adapters.rs"));
for f in [
"pub fn map_project_to_remote",
"pub fn map_project_from_remote",
] {
assert_eq!(
source.matches(&format!("{f}(")).count(),
1,
"{f} must be *defined* exactly once, in this module, for both trackers"
);
}
for (name, text) in [
("glab.rs", include_str!("../glab.rs")),
("linear.rs", include_str!("../linear.rs")),
] {
assert!(
!text.contains("fn map_project_to_remote")
&& !text.contains("fn map_project_from_remote"),
"{name} must not define its own mapping seam"
);
assert!(
text.contains("fn set_projects"),
"{name} must implement the shared membership seam"
);
}
}
#[test]
fn t037_a_report_records_a_project_diagnostic_and_prints_the_field() {
let mut report = SyncReport::default().diagnostics_in_report();
let mut sink: Vec<u8> = Vec::new();
report
.note_project(map_project_to_remote(Tool::GitLab, 2, "p-1"), &mut sink)
.unwrap();
assert_eq!(report.diagnostics.len(), 1);
assert_eq!(report.handled, 0);
let text = String::from_utf8(sink).unwrap();
assert!(text.contains("gitlab: projects"), "{text}");
assert!(text.contains("p-1"), "{text}");
assert_eq!(report.diagnostics[0].field, GITLAB_PROJECT_FIELD);
}
#[test]
fn t037_an_epic_reference_maps_back_to_its_local_id() {
for tool in [Tool::GitLab, Tool::Linear] {
let remote = RemoteEpic {
id: "e-1".to_string(),
name: Some("One".to_string()),
};
match map_epic_from_remote(tool, &remote) {
MappedEpic::Remote { field, value } => {
assert_eq!(field, crate::epics::EPICS_KEY);
assert_eq!(value, "e-1", "the id, never the remote's name");
}
MappedEpic::Unsupported(d) => {
panic!("{tool}: a reported id must map back: {d}")
}
}
}
}
#[test]
fn t037_every_epic_reference_diagnoses_with_its_field_its_tool_and_its_reason() {
for tool in [Tool::GitLab, Tool::Linear] {
match map_epic_to_remote(tool, 3, "e-7") {
MappedEpic::Remote { field, value } => {
assert!(
EPIC_WIRE_VERIFIED.contains(&tool),
"{tool}: a reference mapped without a verified epic field"
);
assert_eq!(field, EPIC_FIELD);
assert_eq!(value, "e-7");
}
MappedEpic::Unsupported(d) => {
assert_eq!(d.tool, tool, "the diagnostic names the tool");
assert_eq!(d.field, EPIC_FIELD, "and the field");
assert_eq!(
d.relation_id, "3->e-7",
"and the issue and the reference it is about"
);
let text = d.to_string();
for needle in [d.field.as_str(), "e-7", "#3"] {
assert!(text.contains(needle), "`{needle}` missing from: {text}");
}
assert!(
!d.message.contains("not verified"),
"{tool}: there is no epic verb to verify: {text}"
);
}
}
}
assert!(epic_absence_reason(Tool::GitLab).contains("epic_iid"));
assert!(epic_absence_reason(Tool::Linear).contains("project"));
assert_ne!(
epic_absence_reason(Tool::GitLab),
epic_absence_reason(Tool::Linear)
);
}
#[test]
fn t037_an_epic_reference_with_no_id_is_diagnosed_and_kept() {
for tool in [Tool::GitLab, Tool::Linear] {
match map_epic_to_remote(tool, 4, " ") {
MappedEpic::Remote { .. } => panic!("{tool}: nothing to send"),
MappedEpic::Unsupported(d) => {
assert_eq!(d.tool, tool);
assert_eq!(d.field, EPIC_FIELD);
assert!(d.message.contains("no id"), "{}", d.message);
assert!(
d.message.contains("kept locally"),
"and says the entry survives: {}",
d.message
);
}
}
}
}
#[test]
fn t037_remote_epic_reading_keeps_order_and_accepts_every_known_shape() {
let list: serde_json::Value =
serde_json::from_str(r#"["e-1", {"id": "e-2", "name": "Two"}, 3]"#).unwrap();
assert_eq!(
remote_epics(Some(&list)),
vec![
RemoteEpic {
id: "e-1".into(),
name: None
},
RemoteEpic {
id: "e-2".into(),
name: Some("Two".into())
},
RemoteEpic {
id: "3".into(),
name: None
},
],
"the payload's order is the list's order"
);
for raw in [
r#"{"identifier": "e-9"}"#,
r#"{"epic_id": "e-9"}"#,
r#""e-9""#,
] {
let value: serde_json::Value = serde_json::from_str(raw).unwrap();
assert_eq!(
remote_epics(Some(&value)),
vec![RemoteEpic {
id: "e-9".into(),
name: None
}],
"{raw}"
);
}
let named: serde_json::Value = serde_json::from_str(r#"[{"name": "Unidentified"}]"#).unwrap();
assert_eq!(
remote_epics(Some(&named)),
vec![RemoteEpic {
id: String::new(),
name: Some("Unidentified".into())
}]
);
assert_eq!(remote_epics(None), [] as [crate::sync::RemoteEpic; 0]);
assert_eq!(
remote_epics(Some(&serde_json::Value::Null)),
[] as [crate::sync::RemoteEpic; 0]
);
assert_eq!(
remote_epics(Some(&serde_json::json!([true, false]))),
[] as [crate::sync::RemoteEpic; 0]
);
}
#[test]
fn t037_a_remote_epic_with_no_id_is_diagnosed_never_guessed_at() {
for tool in [Tool::GitLab, Tool::Linear] {
let remote = RemoteEpic {
id: " ".to_string(),
name: Some("Named but unidentified".to_string()),
};
match map_epic_from_remote(tool, &remote) {
MappedEpic::Remote { .. } => {
panic!("{tool}: an absent id cannot become a local reference")
}
MappedEpic::Unsupported(d) => {
assert_eq!(d.tool, tool);
assert_eq!(d.field, EPIC_FIELD);
assert!(d.message.contains("Named but unidentified"));
assert!(d.message.contains("no id"));
}
}
}
}
#[test]
fn t038_a_remote_epic_diagnostic_is_scrubbed() {
let mut diagnostic = RemoteMappingDiagnostic {
relation_id: "1->lin-secret".to_string(),
field: EPIC_FIELD.to_string(),
tool: Tool::Linear,
message: "the remote reports #1 in epic `e-1` (`lin-secret`)".to_string(),
};
scrub_diagnostic(&mut diagnostic, Some("lin-secret"));
assert!(
diagnostic.message.contains("<token>"),
"{}",
diagnostic.message
);
assert!(!diagnostic.message.contains("lin-secret"));
assert!(!diagnostic.relation_id.contains("lin-secret"));
}
#[test]
fn t038_no_tracker_sends_an_epic_reference_and_both_adapters_share_the_seam() {
for tool in [Tool::GitLab, Tool::Linear] {
assert!(!EPIC_WIRE_VERIFIED.contains(&tool));
assert!(matches!(
map_epic_to_remote(tool, 1, "e-1"),
MappedEpic::Unsupported(_)
));
}
let source = concat!(include_str!("../mod.rs"), include_str!("../adapters.rs"));
for f in ["pub fn map_epic_to_remote", "pub fn map_epic_from_remote"] {
assert_eq!(
source.matches(&format!("{f}(")).count(),
1,
"{f} must be *defined* exactly once, in this module, for both trackers"
);
}
for (name, text) in [
("glab.rs", include_str!("../glab.rs")),
("linear.rs", include_str!("../linear.rs")),
] {
assert!(
!text.contains("fn map_epic_to_remote") && !text.contains("fn map_epic_from_remote"),
"{name} must not define its own mapping seam"
);
assert!(
text.contains("fn set_epics"),
"{name} must implement the shared epic seam"
);
}
}
#[test]
fn t038_a_report_records_an_epic_diagnostic_and_prints_the_field() {
let mut report = SyncReport::default().diagnostics_in_report();
let mut sink: Vec<u8> = Vec::new();
report
.note_epic(map_epic_to_remote(Tool::GitLab, 2, "e-1"), &mut sink)
.unwrap();
assert_eq!(report.diagnostics.len(), 1);
assert_eq!(report.handled, 0);
let text = String::from_utf8(sink).unwrap();
assert!(text.contains("gitlab: epics"), "{text}");
assert!(text.contains("e-1"), "{text}");
assert_eq!(report.diagnostics[0].field, EPIC_FIELD);
}