use onetaskgraph_core::{
GlobalId, PageToken, Predicate, QueryPlan, QueryResponse, SCHEMA_BUNDLE_VERSION, SourceFailure,
SourcePlan, plugin_for, plugin_kinds, registry, schema_bundle,
};
use onetaskgraph_plugin_api::{
NativeId, SecretResolver, SourceError, SourceName, Status, StatusCategory, Task,
};
use secrecy::SecretString;
struct NoSecrets;
impl SecretResolver for NoSecrets {
fn get(&self, _var: &str) -> Option<SecretString> {
None
}
}
fn source(name: &str) -> SourceName {
SourceName::new(name).expect("a valid source name")
}
#[test]
fn a_global_id_renders_as_source_colon_native() {
let id = GlobalId::new(source("work"), NativeId::from("ENG-1"));
assert_eq!(id.to_string(), "work:ENG-1");
assert_eq!(String::from(id.clone()), "work:ENG-1");
assert_eq!(id.source.as_str(), "work");
assert_eq!(id.native.as_str(), "ENG-1");
}
#[test]
fn a_global_id_parses_by_splitting_on_the_first_colon_so_a_native_id_may_contain_them() {
let id: GlobalId = "notes:urn:task:7".parse().expect("parses");
assert_eq!(id.source.as_str(), "notes");
assert_eq!(id.native.as_str(), "urn:task:7");
assert_eq!(id.to_string(), "notes:urn:task:7");
}
#[test]
fn an_unqualified_or_malformed_id_is_refused_with_a_suggested_form() {
for (input, expected) in [
("ENG-1", "write it as <source>:<id>"),
("work:", "names a source but no id"),
("WORK:ENG-1", "source name"),
] {
let Err(SourceError::Config { message }) = input.parse::<GlobalId>() else {
panic!("{input:?} is not a usable qualified id");
};
assert!(message.contains(expected), "{input:?}: {message}");
}
}
#[test]
fn a_global_id_round_trips_through_json_as_a_single_string() {
let id = GlobalId::new(source("gh-main"), NativeId::from("PVTI_1"));
let encoded = serde_json::to_string(&id).expect("encodes");
assert_eq!(encoded, "\"gh-main:PVTI_1\"");
assert_eq!(
serde_json::from_str::<GlobalId>(&encoded).expect("decodes"),
id
);
assert!(serde_json::from_str::<GlobalId>("\"unqualified\"").is_err());
}
#[test]
fn a_string_that_is_not_this_engines_resume_document_is_refused_where_it_enters() {
let Err(SourceError::Malformed { message }) = PageToken::parse("{not json") else {
panic!("a string that is not this engine's document must be refused");
};
assert!(
message.contains("not a page token this engine writes"),
"{message}"
);
for unissued in [
"7b22776f726b223a2230227d",
"5b7b22736f75726365223a224241445f4e414d45222c2273747265616d223a226974656d73227d5d",
"5b5d",
] {
let Err(SourceError::Malformed { .. }) = PageToken::parse(unissued) else {
panic!("a token this engine did not issue must be refused: {unissued}");
};
}
let Err(error) = serde_json::from_str::<PageToken>(r#""{not json""#) else {
panic!("deserialising a string that is not this engine's document must be refused");
};
assert!(
error
.to_string()
.contains("not a page token this engine writes"),
"{error}"
);
}
#[test]
fn the_registry_names_every_plugin_kind_this_build_knows() {
assert_eq!(
plugin_kinds(),
[
"github-projects",
"in-memory",
"linear",
"local-md",
"subprocess"
]
);
assert_eq!(registry().len(), 5);
}
#[test]
fn the_registry_resolves_a_kind_to_its_plugin_and_nothing_to_an_unknown_one() {
let plugin = plugin_for("in-memory").expect("the in-memory plugin is registered");
assert_eq!(plugin.kind(), "in-memory");
let built = plugin
.build(&source("notes"), &serde_json::json!({}), &NoSecrets)
.expect("an empty in-memory source is valid");
assert_eq!(built.kind(), "in-memory");
assert!(plugin_for("jira").is_none());
}
const PUBLISHED_BUNDLES: &[(u32, &[&str])] = &[
(1, &FIRST_BUNDLE_ROOTS),
(2, &SECOND_BUNDLE_ROOTS),
(3, &THIRD_BUNDLE_ROOTS),
(4, &FOURTH_BUNDLE_ROOTS),
(5, &FIFTH_BUNDLE_ROOTS),
(6, &SIXTH_BUNDLE_ROOTS),
(7, &SEVENTH_BUNDLE_ROOTS),
(8, &EIGHTH_BUNDLE_ROOTS),
];
const FIRST_BUNDLE_ROOTS: [&str; 26] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"QueryResponseOfTask",
"QueryResponseOfProject",
"QueryResponseOfLabel",
];
const SECOND_BUNDLE_ROOTS: [&str; 33] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"QueryResponseOfTask",
"QueryResponseOfProject",
"QueryResponseOfLabel",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
];
const THIRD_BUNDLE_ROOTS: [&str; 42] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
"PageToken",
"QualifiedTask",
"QualifiedProject",
"QualifiedLabel",
"QualifiedEdge",
"SearchHit",
"SourceListing",
"QueryResponseOfQualifiedTask",
"QueryResponseOfQualifiedProject",
"QueryResponseOfQualifiedLabel",
"QueryResponseOfQualifiedEdge",
"QueryResponseOfSearchHit",
];
const FOURTH_BUNDLE_ROOTS: [&str; 44] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
"PageToken",
"QualifiedTask",
"QualifiedProject",
"QualifiedLabel",
"QualifiedEdge",
"SearchHit",
"SourceListing",
"QueryResponseOfQualifiedTask",
"QueryResponseOfQualifiedProject",
"QueryResponseOfQualifiedLabel",
"QueryResponseOfQualifiedEdge",
"QueryResponseOfSearchHit",
"TextFields",
"SearchKind",
];
const FIFTH_BUNDLE_ROOTS: [&str; 45] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
"PageToken",
"QualifiedTask",
"QualifiedProject",
"QualifiedLabel",
"QualifiedEdge",
"SearchHit",
"SourceListing",
"SourceListings",
"QueryResponseOfQualifiedTask",
"QueryResponseOfQualifiedProject",
"QueryResponseOfQualifiedLabel",
"QueryResponseOfQualifiedEdge",
"QueryResponseOfSearchHit",
"TextFields",
"SearchKind",
];
const SIXTH_BUNDLE_ROOTS: [&str; 49] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyEndpoint",
"QualifiedEndpoint",
"ItemKind",
"Repository",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
"PageToken",
"QualifiedTask",
"QualifiedProject",
"QualifiedLabel",
"QualifiedEdge",
"SearchHit",
"SourceListing",
"SourceListings",
"QueryResponseOfQualifiedTask",
"QueryResponseOfQualifiedProject",
"QueryResponseOfQualifiedLabel",
"QueryResponseOfQualifiedEdge",
"QueryResponseOfSearchHit",
"TextFields",
"SearchKind",
];
const SEVENTH_BUNDLE_ROOTS: [&str; 52] = [
"Task",
"Project",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyEndpoint",
"QualifiedEndpoint",
"ItemKind",
"Repository",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
"PageToken",
"QualifiedTask",
"QualifiedProject",
"QualifiedLabel",
"QualifiedEdge",
"SearchHit",
"SourceListing",
"SourceListings",
"QueryResponseOfQualifiedTask",
"QueryResponseOfQualifiedProject",
"QueryResponseOfQualifiedLabel",
"QueryResponseOfQualifiedEdge",
"QueryResponseOfSearchHit",
"TextFields",
"SearchKind",
"CopyReport",
"CopyOutcome",
"CopyAction",
];
const EIGHTH_BUNDLE_ROOTS: [&str; 56] = [
"Task",
"Project",
"Document",
"Location",
"Label",
"Status",
"StatusCategory",
"DependencyEdge",
"DependencyEndpoint",
"QualifiedEndpoint",
"ItemKind",
"Repository",
"DependencyKind",
"Direction",
"TaskQuery",
"ProjectQuery",
"DocumentQuery",
"PageRequest",
"PageOfTask",
"PageOfProject",
"PageOfDocument",
"PageOfLabel",
"PageOfDependencyEdge",
"Capabilities",
"Health",
"SourceError",
"GlobalId",
"QueryPlan",
"SourcePlan",
"Predicate",
"SourceFailure",
"EffectiveConfig",
"Setting",
"Origin",
"OutputFormat",
"SecretsReport",
"ResolvedCredential",
"CredentialLayer",
"PageToken",
"QualifiedTask",
"QualifiedProject",
"QualifiedLabel",
"QualifiedEdge",
"SearchHit",
"SourceListing",
"SourceListings",
"QueryResponseOfQualifiedTask",
"QueryResponseOfQualifiedProject",
"QueryResponseOfQualifiedLabel",
"QueryResponseOfQualifiedEdge",
"QueryResponseOfSearchHit",
"TextFields",
"SearchKind",
"CopyReport",
"CopyOutcome",
"CopyAction",
];
#[test]
fn the_schema_bundle_describes_every_contract_root_and_every_plugin_config() {
let bundle = schema_bundle();
assert_eq!(bundle["version"], SCHEMA_BUNDLE_VERSION);
let roots = bundle["roots"].as_object().expect("roots is an object");
let sorted = |names: &[&str]| {
let mut owned: Vec<String> = names.iter().map(|name| (*name).to_owned()).collect();
owned.sort_unstable();
owned
};
assert_eq!(
SCHEMA_BUNDLE_VERSION as usize,
PUBLISHED_BUNDLES.len(),
"SCHEMA_BUNDLE_VERSION is {SCHEMA_BUNDLE_VERSION} but PUBLISHED_BUNDLES has {} row(s). \
Append a row for the new shape and bump the version to match.",
PUBLISHED_BUNDLES.len()
);
for (index, (version, published)) in PUBLISHED_BUNDLES.iter().enumerate() {
assert_eq!(
*version as usize,
index + 1,
"PUBLISHED_BUNDLES row {index} claims version {version}; rows are the versions \
in order, so row {index} is version {}.",
index + 1
);
assert!(
!PUBLISHED_BUNDLES[..index]
.iter()
.any(|(_, earlier)| sorted(earlier) == sorted(published)),
"version {version} republishes an earlier version's exact root set; if the \
shape did not change, the version must not either."
);
}
let (_, expected) = PUBLISHED_BUNDLES
.iter()
.find(|(version, _)| *version == SCHEMA_BUNDLE_VERSION)
.expect("PUBLISHED_BUNDLES lists the current SCHEMA_BUNDLE_VERSION");
assert_eq!(
sorted(&roots.keys().map(String::as_str).collect::<Vec<_>>()),
sorted(expected),
"the bundle's roots are not what version {SCHEMA_BUNDLE_VERSION} publishes. Append a \
row to PUBLISHED_BUNDLES with the new set and bump SCHEMA_BUNDLE_VERSION to match — \
an SDK generated against the old version would otherwise silently emit the wrong \
models."
);
for root in expected.iter() {
assert!(roots[*root].is_object(), "the bundle is missing {root}");
}
let plugins = bundle["plugin_config"]
.as_object()
.expect("plugin_config is an object");
let mut kinds: Vec<&String> = plugins.keys().collect();
kinds.sort();
assert_eq!(
kinds,
[
"github-projects",
"in-memory",
"linear",
"local-md",
"subprocess"
]
);
}
#[test]
fn a_response_carries_the_plan_that_produced_it_and_round_trips() {
let response = QueryResponse {
items: vec![Task {
id: NativeId::from("ENG-1"),
title: "Land the contract".to_owned(),
content: None,
status: Status {
category: StatusCategory::InProgress,
name: "In Review".to_owned(),
},
labels: Vec::new(),
project: None,
url: None,
location: None,
created_at: None,
updated_at: None,
metadata: Default::default(),
repositories: Vec::new(),
}],
next: Some(
PageToken::parse("7b227175657279223a2230313233343536373839616263646566222c2273747265616d73223a5b7b22736f75726365223a22776f726b222c2273747265616d223a226974656d73222c22637572736f72223a223530227d5d7d")
.expect("a token this engine issued"),
),
plan: QueryPlan {
per_source: vec![
SourcePlan {
source: source("work"),
kind: "in-memory".to_owned(),
pushed_down: vec![Predicate::Label, Predicate::Status],
applied_locally: Vec::new(),
emulated: Vec::new(),
unavailable: Vec::new(),
pages_fetched: 1,
},
SourcePlan {
source: source("notes"),
kind: "local-md".to_owned(),
pushed_down: Vec::new(),
applied_locally: vec![Predicate::Label, Predicate::SearchContent],
emulated: vec![Predicate::ReverseDependencies],
unavailable: vec![Predicate::Project],
pages_fetched: 4,
},
],
},
errors: vec![SourceFailure {
source: source("gh-main"),
error: SourceError::Unavailable {
message: "no route to host".to_owned(),
},
}],
};
let encoded = serde_json::to_string(&response).expect("encodes");
let decoded: QueryResponse<Task> = serde_json::from_str(&encoded).expect("decodes");
assert_eq!(decoded, response);
assert_eq!(decoded.items.len(), 1);
assert_eq!(decoded.errors.len(), 1);
assert_eq!(decoded.plan.per_source.len(), 2);
assert_eq!(QueryPlan::default().per_source, Vec::new());
}
#[test]
fn every_predicate_serialises_as_kebab_case_so_explain_output_is_stable() {
for (predicate, wire) in [
(Predicate::Label, "label"),
(Predicate::Status, "status"),
(Predicate::SearchTitle, "search-title"),
(Predicate::SearchContent, "search-content"),
(Predicate::Project, "project"),
(Predicate::ReverseDependencies, "reverse-dependencies"),
] {
assert_eq!(
serde_json::to_value(predicate).expect("encodes"),
serde_json::json!(wire)
);
assert_eq!(
serde_json::from_value::<Predicate>(serde_json::json!(wire)).expect("decodes"),
predicate
);
}
}