use super::*;
use gobby_core::ai_types::TokenUsage;
use crate::explainer::{ExplainerPrompt, ExplainerResponse};
use crate::provenance::ProvenanceGraph;
use crate::session::{AcceptedResearchNote, ResearchScope, ResearchSession};
use crate::sources::{SourceDraft, SourceKind, SourceManifest};
fn session_with_note(scope: &ResearchScope, title: &str, relative_path: &str) -> ResearchSession {
ResearchSession {
session_id: "research-compile-test".to_string(),
question: "How should compile handoff work?".to_string(),
prompt: "Compile source-grounded research".to_string(),
scope: scope.clone(),
source_constraints: vec!["accepted notes only".to_string()],
agent_count: 1,
dispatch_task_id: Some("#302".to_string()),
dispatch: None,
accepted_notes: vec![AcceptedResearchNote {
title: title.to_string(),
path: scope.root().join(relative_path),
code_citations: Vec::new(),
degradation: None,
}],
compile_state: None,
}
}
#[test]
fn compile_bundle_contains_required_sections() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(
¬e_path,
"---\ntitle: Compile behavior\nsource: daemon notes\n---\n\nCitation: Example Docs, Compile API\nConflict: Workers disagree about overwrite behavior.\nGap: Missing benchmark evidence.\nAccepted chunk about durable synthesis handoff.",
)
.expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = prepare_handoff(
&mut session,
CompileRequest {
topic: "Compile behavior".to_string(),
outline: vec![
"Durable handoff".to_string(),
"Synthesis inputs".to_string(),
],
target_page: Some(PathBuf::from("compile-behavior.md")),
write_intent: false,
},
)
.expect("compile handoff prepared");
assert_eq!(outcome.bundle.outline.len(), 2);
assert_eq!(outcome.bundle.accepted_sources.len(), 1);
assert_eq!(outcome.bundle.citations, vec!["Example Docs, Compile API"]);
assert_eq!(
outcome.bundle.conflicting_claims,
vec!["Workers disagree about overwrite behavior."]
);
assert_eq!(
outcome.bundle.missing_evidence,
vec!["Missing benchmark evidence."]
);
let rendered = std::fs::read_to_string(&outcome.bundle.path).expect("bundle written");
assert!(rendered.contains("## Topic outline"));
assert!(rendered.contains("## Accepted sources"));
assert!(rendered.contains("## Citations"));
assert!(rendered.contains("## Conflicting claims"));
assert!(rendered.contains("## Missing evidence"));
}
#[test]
fn compile_handoff_is_non_destructive_by_default() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let page_path = scope.root().join("compile-behavior.md");
std::fs::write(&page_path, "human-authored wiki page").expect("page written");
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Citation: Example Docs").expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = prepare_handoff(
&mut session,
CompileRequest {
topic: "Compile behavior".to_string(),
outline: vec!["Durable handoff".to_string()],
target_page: Some(PathBuf::from("compile-behavior.md")),
write_intent: false,
},
)
.expect("compile handoff prepared");
assert_eq!(
std::fs::read_to_string(&page_path).expect("page retained"),
"human-authored wiki page"
);
assert_ne!(outcome.bundle.path, page_path);
assert!(!outcome.state.write_intent);
}
#[test]
fn prepare_handoff_does_not_write_target_page() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let page_path = scope.root().join("compile-behavior.md");
std::fs::write(&page_path, "human-authored wiki page").expect("page written");
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Citation: Example Docs").expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = prepare_handoff(
&mut session,
CompileRequest {
topic: "Compile behavior".to_string(),
outline: vec!["Durable handoff".to_string()],
target_page: Some(PathBuf::from("compile-behavior.md")),
write_intent: true,
},
)
.expect("compile handoff prepared");
assert_eq!(
std::fs::read_to_string(&page_path).expect("page retained"),
"human-authored wiki page"
);
assert!(outcome.state.write_intent);
}
#[test]
fn compile_fails_on_out_of_scope_accepted_note() {
let in_scope = tempfile::tempdir().expect("in scope tempdir");
let out_of_scope = tempfile::tempdir().expect("out of scope tempdir");
let scope = ResearchScope::project_for_id("project-1", in_scope.path());
let in_scope_path = scope.root().join("raw/research/in-scope.md");
std::fs::create_dir_all(in_scope_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(&in_scope_path, "Citation: In-scope citation").expect("note written");
let mut session = session_with_note(&scope, "In scope", "raw/research/in-scope.md");
session.accepted_notes.push(AcceptedResearchNote {
title: "Out of scope".to_string(),
path: out_of_scope.path().join("raw/research/out-of-scope.md"),
code_citations: Vec::new(),
degradation: None,
});
let out_path = out_of_scope.path().join("raw/research/out-of-scope.md");
std::fs::create_dir_all(out_path.parent().expect("out parent")).expect("out raw dir");
std::fs::write(&out_path, "Out of scope citation").expect("out note written");
let err = prepare_handoff(
&mut session,
CompileRequest {
topic: "Scoped compile".to_string(),
outline: vec!["Scoped sources".to_string()],
target_page: None,
write_intent: false,
},
)
.expect_err("out-of-scope accepted note must fail fast");
assert!(matches!(
err,
WikiError::InvalidInput {
field: "accepted_note",
..
}
));
}
#[test]
fn compile_rejects_absolute_or_escaping_target_pages() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Citation: Example Docs").expect("note written");
let mut absolute_session =
session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let absolute = prepare_handoff(
&mut absolute_session,
CompileRequest {
topic: "Compile behavior".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(scope.root().join("absolute.md")),
write_intent: false,
},
)
.expect_err("absolute target page must be rejected");
assert!(matches!(
absolute,
WikiError::InvalidInput {
field: "target_page",
..
}
));
let mut escaping_session =
session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let escaping = prepare_handoff(
&mut escaping_session,
CompileRequest {
topic: "Compile behavior".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("../outside.md")),
write_intent: false,
},
)
.expect_err("escaping target page must be rejected");
assert!(matches!(
escaping,
WikiError::InvalidInput {
field: "target_page",
..
}
));
}
#[cfg(unix)]
#[test]
fn compile_rejects_target_page_through_symlinked_parent() {
let vault = tempfile::tempdir().expect("vault tempdir");
let outside = tempfile::tempdir().expect("outside tempdir");
std::os::unix::fs::symlink(outside.path(), vault.path().join("linked"))
.expect("symlink outside");
let error = write_target_page(
vault.path(),
&vault.path().join("linked/outside.md"),
"# Outside\n",
)
.expect_err("symlinked target parent rejected");
assert!(matches!(
error,
WikiError::InvalidInput {
field: "target_page",
..
}
));
}
#[cfg(windows)]
#[test]
fn compile_rejects_target_page_through_symlinked_parent() {
let vault = tempfile::tempdir().expect("vault tempdir");
let outside = tempfile::tempdir().expect("outside tempdir");
if let Err(error) =
std::os::windows::fs::symlink_dir(outside.path(), vault.path().join("linked"))
{
if matches!(
error.kind(),
std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::Unsupported
) {
eprintln!("skipping Windows symlink assertion: {error}");
return;
}
panic!("symlink outside: {error}");
}
let error = write_target_page(
vault.path(),
&vault.path().join("linked/outside.md"),
"# Outside\n",
)
.expect_err("symlinked target parent rejected");
assert!(matches!(
error,
WikiError::InvalidInput {
field: "target_page",
..
}
));
}
#[test]
fn compile_writes_obsidian_markdown() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(
¬e_path,
concat!(
"---\n",
"title: Compile behavior\n",
"source: daemon notes\n",
"---\n\n",
"Citation: Example Docs, Compile API\n",
"Compile turns accepted notes into source-grounded wiki articles.\n",
"Evidence sections keep claims traceable to their matching outline entries."
),
)
.expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki(
&mut session,
CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string(), "Evidence".to_string()],
target_page: None,
write_intent: false,
},
)
.expect("wiki articles compiled");
let page = std::fs::read_to_string(&outcome.article_path).expect("article written");
assert!(
outcome
.article_path
.ends_with("knowledge/topics/durable-compile.md")
);
assert!(page.starts_with("---\n"));
assert!(page.contains("title: \"Durable Compile\""));
assert!(page.contains("source_kind: \"topic\""));
assert!(page.contains("[[knowledge/sources/compile-behavior|Compile behavior]]"));
assert!(page.contains("Example Docs, Compile API"));
let source_page = scope.root().join("knowledge/sources/compile-behavior.md");
assert!(source_page.exists());
let provenance =
std::fs::read_to_string(scope.root().join("meta/provenance.json")).expect("provenance");
assert!(provenance.contains("knowledge/topics/durable-compile.md"));
assert!(provenance.contains("raw/research/compile.md"));
let provenance = ProvenanceGraph::load_from_vault(scope.root()).expect("load provenance graph");
let links = provenance.links();
assert_eq!(links.len(), 2);
assert_eq!(links[0].section.section_id, "durable-compile");
assert_eq!(links[1].section.section_id, "evidence");
let article_page = std::path::Path::new("knowledge/topics/durable-compile.md");
assert_eq!(
provenance
.links_for_page_section(article_page, "durable-compile")
.len(),
1
);
assert_eq!(
provenance
.links_for_page_section(article_page, "evidence")
.len(),
1
);
let source = &provenance.links()[0].source;
assert!(source.byte_end > source.byte_start);
}
#[test]
fn compile_reuses_existing_source_digest_pages() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_body = "Compile turns accepted notes into source-grounded wiki articles.\n";
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, note_body).expect("note written");
let record = SourceManifest::register(
scope.root(),
SourceDraft::new(
"raw/research/compile.md",
SourceKind::Text,
"2026-07-05T00:00:00Z",
note_body.as_bytes().to_vec(),
),
)
.expect("source registered");
let digest_path = scope
.root()
.join("knowledge/sources")
.join(format!("{}.md", record.id));
std::fs::create_dir_all(digest_path.parent().expect("digest parent")).expect("sources dir");
std::fs::write(
&digest_path,
"---\ntitle: Compile behavior\n---\n\nRich digest body.\n",
)
.expect("digest written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki(
&mut session,
CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
)
.expect("wiki articles compiled");
let entries: Vec<String> = std::fs::read_dir(scope.root().join("knowledge/sources"))
.expect("sources dir listed")
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(entries, vec![format!("{}.md", record.id)]);
assert!(outcome.source_paths.is_empty());
let article = std::fs::read_to_string(&outcome.article_path).expect("article written");
assert!(
article.contains(&format!(
"[[knowledge/sources/{}|Compile behavior]]",
record.id
)),
"{article}"
);
assert_eq!(
std::fs::read_to_string(&digest_path).expect("digest retained"),
"---\ntitle: Compile behavior\n---\n\nRich digest body.\n"
);
}
#[test]
fn recompile_updates_source_stub_pages_in_place() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "First compile evidence.\n").expect("note written");
let request = || CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("knowledge/topics/durable-compile.md")),
write_intent: true,
};
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
compile_to_wiki(&mut session, request()).expect("first compile succeeded");
std::fs::write(¬e_path, "Recompiled evidence.\n").expect("note rewritten");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki(&mut session, request()).expect("recompile succeeded");
let entries: Vec<String> = std::fs::read_dir(scope.root().join("knowledge/sources"))
.expect("sources dir listed")
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(entries, vec!["compile-behavior.md".to_string()]);
assert_eq!(
outcome.source_paths,
vec![scope.root().join("knowledge/sources/compile-behavior.md")]
);
let stub = std::fs::read_to_string(scope.root().join("knowledge/sources/compile-behavior.md"))
.expect("stub read");
assert!(stub.contains("Recompiled evidence."), "{stub}");
assert!(
stub.contains("source_path: \"raw/research/compile.md\""),
"{stub}"
);
}
#[test]
fn recompile_overwrites_source_stub_without_write_intent() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/shared.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Shared source evidence.\n").expect("note written");
let mut first = session_with_note(&scope, "Shared Source", "raw/research/shared.md");
compile_to_wiki(
&mut first,
CompileRequest {
topic: "First Topic".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("knowledge/topics/first-topic.md")),
write_intent: true,
},
)
.expect("first compile succeeded");
let mut second = session_with_note(&scope, "Shared Source", "raw/research/shared.md");
let outcome = compile_to_wiki(
&mut second,
CompileRequest {
topic: "Second Topic".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("knowledge/topics/second-topic.md")),
write_intent: false,
},
)
.expect("second compile overwrites shared stub without write intent");
let entries: Vec<String> = std::fs::read_dir(scope.root().join("knowledge/sources"))
.expect("sources dir listed")
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(entries, vec!["shared-source.md".to_string()]);
assert_eq!(
outcome.source_paths,
vec![scope.root().join("knowledge/sources/shared-source.md")]
);
}
#[test]
fn recompile_without_target_page_updates_article_in_place() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "First compile evidence.\n").expect("note written");
let request = || CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: true,
};
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let first = compile_to_wiki(&mut session, request()).expect("first compile succeeded");
assert_eq!(
first.article_path,
scope.root().join("knowledge/topics/durable-compile.md")
);
std::fs::write(¬e_path, "Recompiled evidence.\n").expect("note rewritten");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki(&mut session, request()).expect("recompile succeeded");
assert_eq!(outcome.article_path, first.article_path);
let entries: Vec<String> = std::fs::read_dir(scope.root().join("knowledge/topics"))
.expect("topics dir listed")
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(entries, vec!["durable-compile.md".to_string()]);
assert!(
outcome
.prompt
.user
.contains("update it rather than starting over"),
"{}",
outcome.prompt.user
);
}
#[test]
fn compile_after_recapture_emits_single_digest_without_suffix() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let vault_root = scope.root().to_path_buf();
let source_path = vault_root.join("recaptured-note.md");
let mut ai_source = gobby_core::config::EnvOnlySource;
let mut ai_context = gobby_core::ai_context::AiContext::resolve(None, &mut ai_source);
let options = crate::api::IngestFileOptions {
no_ai: true,
..crate::api::IngestFileOptions::default()
};
options.apply_to_ai_context(&mut ai_context);
let scope_identity = scope.identity();
for (body, fetched_at) in [
("# Note\n\nFirst capture body.\n", "2026-07-01T00:00:00Z"),
(
"# Note\n\nSecond capture body, changed.\n",
"2026-07-02T00:00:00Z",
),
] {
std::fs::write(&source_path, body).expect("write source");
let mut store = crate::store::MemoryWikiStore::default();
crate::ingest::file::ingest_path(
&vault_root,
&mut store,
&scope_identity,
&ai_context,
&options,
crate::ingest::file::LocalFileSnapshot {
path: &source_path,
fetched_at,
},
&mut crate::progress::ProgressOptions::default(),
)
.expect("ingest capture");
}
let raw_notes: Vec<PathBuf> = std::fs::read_dir(vault_root.join("raw"))
.expect("raw dir")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.extension().is_some_and(|ext| ext == "md")
&& path.file_name().is_some_and(|name| name != "INDEX.md")
})
.collect();
assert_eq!(raw_notes.len(), 1, "re-capture keeps a single raw source");
let mut session = session_with_note(&scope, "Recaptured note", "raw/research/unused.md");
session.accepted_notes = raw_notes
.iter()
.map(|path| AcceptedResearchNote {
title: "Recaptured note".to_string(),
path: path.clone(),
code_citations: Vec::new(),
degradation: None,
})
.collect();
compile_to_wiki(
&mut session,
CompileRequest {
topic: "Recaptured Note Topic".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("knowledge/topics/recaptured-note.md")),
write_intent: true,
},
)
.expect("compile succeeded");
let digests: Vec<String> = std::fs::read_dir(vault_root.join("knowledge/sources"))
.expect("sources dir listed")
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(
digests.len(),
1,
"single digest page, no -2 sibling: {digests:?}"
);
assert!(!digests[0].ends_with("-2.md"), "{digests:?}");
}
#[test]
fn recompile_of_machine_page_overwrites_without_write_intent() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "First compile evidence.\n").expect("note written");
let request = |write_intent: bool| CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent,
};
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let first = compile_to_wiki(&mut session, request(true)).expect("first compile succeeded");
assert!(
std::fs::read_to_string(&first.article_path)
.expect("article written")
.contains("synthesis_mode:"),
"first compile marks the page machine-owned"
);
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let second = compile_to_wiki(&mut session, request(false))
.expect("recompile of a machine-owned page overwrites without write intent");
assert_eq!(second.article_path, first.article_path);
let articles: Vec<String> =
std::fs::read_dir(first.article_path.parent().expect("article parent"))
.expect("article dir listed")
.filter_map(Result::ok)
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.filter(|name| name.ends_with(".md"))
.collect();
assert_eq!(articles.len(), 1, "no slug-suffixed sibling: {articles:?}");
}
#[test]
fn recompile_over_hand_authored_page_requires_write_intent() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "First compile evidence.\n").expect("note written");
let target = PathBuf::from("knowledge/topics/hand-authored.md");
let page_path = scope.root().join(&target);
std::fs::create_dir_all(page_path.parent().expect("target parent")).expect("target dir");
let curated = "# Hand authored\n\nCurated by a human; no synthesis_mode.\n";
std::fs::write(&page_path, curated).expect("page written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let error = compile_to_wiki(
&mut session,
CompileRequest {
topic: "Hand Authored".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(target.clone()),
write_intent: false,
},
)
.expect_err("recompile over a hand-authored page fails loud without write intent");
assert_eq!(error.code(), "invalid_input");
assert_eq!(
std::fs::read_to_string(&page_path).expect("page retained"),
curated,
"the human page is never clobbered"
);
}
#[test]
fn recompile_carries_existing_target_body_into_prompt() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Compile evidence.\n").expect("note written");
let target_path = scope.root().join("knowledge/topics/durable-compile.md");
std::fs::create_dir_all(target_path.parent().expect("target parent")).expect("topics dir");
std::fs::write(
&target_path,
"---\ntitle: Stale Title\n---\n\n## Overview\n\nPreviously compiled claim.\n",
)
.expect("target written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("knowledge/topics/durable-compile.md")),
write_intent: true,
},
WikiCompileOptions::default(),
None,
)
.expect("recompile succeeded");
assert!(
outcome.prompt.user.contains("Current page content"),
"{}",
outcome.prompt.user
);
assert!(outcome.prompt.user.contains("Previously compiled claim."));
assert!(!outcome.prompt.user.contains("Stale Title"));
}
#[test]
fn compile_regenerates_index_catalog_from_vault_state() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let topics_dir = scope.root().join("knowledge/topics");
std::fs::create_dir_all(&topics_dir).expect("topics dir");
std::fs::write(
topics_dir.join("existing.md"),
"---\ntitle: \"Existing Entry\"\n---\n\nAlready compiled body.\n",
)
.expect("existing page written");
let note_path = scope.root().join("raw/research/index.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Index updates keep unrelated entries.").expect("note written");
let mut session = session_with_note(&scope, "Index behavior", "raw/research/index.md");
compile_to_wiki(
&mut session,
CompileRequest {
topic: "Index Preservation".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
)
.expect("wiki article compiled");
let index = std::fs::read_to_string(scope.root().join("_index.md")).expect("index read");
assert!(index.contains("## Overview"), "{index}");
assert!(
index.contains("[[knowledge/topics/existing|Existing Entry]]"),
"{index}"
);
assert!(
index.contains("[[knowledge/topics/index-preservation|Index Preservation]]"),
"{index}"
);
let knowledge = std::fs::read_to_string(scope.root().join("knowledge/INDEX.md"))
.expect("knowledge index read");
assert!(
knowledge.contains("[[knowledge/topics/index-preservation|Index Preservation]]"),
"{knowledge}"
);
}
#[test]
fn compile_without_checkpoint_persistence_leaves_research_session_untouched() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/ephemeral.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Ephemeral compile evidence.").expect("note written");
let mut session = session_with_note(&scope, "Ephemeral", "raw/research/ephemeral.md");
compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Ephemeral Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
WikiCompileOptions {
persist_checkpoint: false,
..WikiCompileOptions::default()
},
None,
)
.expect("wiki article compiled");
assert!(session.compile_state.is_some());
let checkpoint = ResearchSession::checkpoint_path(scope.root());
assert!(
!checkpoint.exists(),
"persist_checkpoint=false must not write {}",
checkpoint.display()
);
}
#[test]
fn compile_appends_page_write_log_entries() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/logged.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Compile writes go to the log.").expect("note written");
let mut session = session_with_note(&scope, "Logged compile", "raw/research/logged.md");
compile_to_wiki(
&mut session,
CompileRequest {
topic: "Logged Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
)
.expect("wiki article compiled");
let log = std::fs::read_to_string(scope.root().join("log.md")).expect("log read");
let article_line = log
.lines()
.find(|line| line.contains("knowledge/topics/logged-compile.md"))
.expect("article write logged");
assert!(article_line.starts_with("- "), "{article_line}");
assert!(article_line.contains("page_created:"), "{article_line}");
assert!(article_line.contains("Logged Compile"), "{article_line}");
}
#[test]
fn write_target_page_rejects_existing_page_without_overwrite_race() {
let vault = tempfile::tempdir().expect("vault tempdir");
let target = vault.path().join("existing.md");
std::fs::write(&target, "human-authored wiki page").expect("existing page");
let error = write_target_page(vault.path(), &target, "# Replacement\n")
.expect_err("existing target rejected");
assert!(matches!(
error,
WikiError::InvalidInput {
field: "write_intent",
..
}
));
assert_eq!(
std::fs::read_to_string(&target).expect("existing page retained"),
"human-authored wiki page"
);
}
#[test]
fn compile_explainer_generates_grounded_prose_sections() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(
¬e_path,
"---\ntitle: Compile behavior\n---\n\nCompile turns accepted notes into grounded articles.",
)
.expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let mut prompts = Vec::new();
let outcome = {
let mut generator = |prompt: &ExplainerPrompt| {
prompts.push(prompt.user.clone());
Ok(ExplainerResponse {
text: "## Overview\nCompile grounds articles in accepted notes \
[source: raw/research/compile.md]. It never keeps invented citations \
[source: raw/research/invented.md].\n"
.to_string(),
model: Some("mock-model".to_string()),
route: "daemon",
tool_use_count: Some(4),
turns: Some(3),
usage: Some(TokenUsage {
input_tokens: Some(120),
output_tokens: Some(45),
total_tokens: Some(165),
}),
})
};
compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Durable Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
WikiCompileOptions::default(),
Some(&mut generator),
)
.expect("wiki article compiled")
};
let page = std::fs::read_to_string(&outcome.article_path).expect("article written");
assert!(page.contains("synthesis_mode: \"daemon\""), "{page}");
assert!(!page.contains("degraded:"), "{page}");
assert!(page.contains("## Overview"), "{page}");
assert!(
page.contains("accepted notes [[knowledge/sources/compile-behavior|Compile behavior]]."),
"{page}"
);
assert!(!page.contains("[source:"), "{page}");
assert!(!page.contains("invented.md"), "{page}");
let report = outcome.explainer.expect("explainer report");
assert_eq!(report.status, "generated");
assert_eq!(report.route, Some("daemon"));
assert_eq!(report.model.as_deref(), Some("mock-model"));
assert_eq!(report.tool_use_count, Some(4));
assert_eq!(report.turns, Some(3));
assert_eq!(
report.usage.as_ref().and_then(TokenUsage::token_count),
Some(165)
);
assert_eq!(report.citations_kept, 1);
assert_eq!(report.citations_stripped, 1);
assert!(outcome.prompt.tokens_estimated > 0);
assert_eq!(outcome.prompt.truncated_sources, 0);
let prompt_user = prompts.first().expect("explainer prompt captured");
assert!(
prompt_user.contains("[source: raw/research/compile.md]"),
"{prompt_user}"
);
assert!(
prompt_user.contains("Compile turns accepted notes"),
"{prompt_user}"
);
}
#[test]
fn compile_explainer_failure_degrades_and_keeps_structural_skeleton() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Accepted compile evidence.").expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let mut generator = |_prompt: &ExplainerPrompt| {
Err::<ExplainerResponse, _>("text lane unavailable".to_string())
};
let outcome = compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Degraded Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
WikiCompileOptions::default(),
Some(&mut generator),
)
.expect("wiki article compiled despite explainer failure");
let page = std::fs::read_to_string(&outcome.article_path).expect("article written");
assert!(page.contains("synthesis_mode: \"fallback\""), "{page}");
assert!(page.contains("degraded: true"), "{page}");
assert!(page.contains("degraded_sources:"), "{page}");
assert!(page.contains(" - model_provider_unavailable"), "{page}");
assert!(page.contains("## Overview"), "{page}");
let report = outcome.explainer.expect("explainer report");
assert_eq!(report.status, "failed");
assert_eq!(report.error.as_deref(), Some("text lane unavailable"));
assert_eq!(report.citations_kept, 0);
}
#[test]
fn compile_lane_b_failure_hard_fails_without_skeleton() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Accepted compile evidence.").expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let mut generator = |_prompt: &ExplainerPrompt| {
Err::<ExplainerResponse, _>("tool loop unavailable".to_string())
};
let error = compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Hard Fail Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: Some(PathBuf::from("hard-fail-compile.md")),
write_intent: true,
},
WikiCompileOptions {
hard_fail_on_generation_failure: true,
..WikiCompileOptions::default()
},
Some(&mut generator),
)
.expect_err("Lane B failure hard-fails");
assert!(
matches!(error, crate::WikiError::Generation { .. }),
"expected a generation error, got: {error}"
);
let message = error.to_string();
assert!(
message.contains("Lane B compile generation failed"),
"{message}"
);
assert!(message.contains("no skeleton"), "{message}");
assert!(
!scope.root().join("knowledge").exists(),
"no skeleton article should be written on Lane B hard-fail"
);
assert!(
!scope.root().join("hard-fail-compile.md").exists(),
"write-intent target handoff should not be written on Lane B hard-fail"
);
}
#[test]
fn compile_drops_alias_equal_to_title_and_keeps_case_variants() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Accepted compile evidence.").expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Gcode".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
WikiCompileOptions {
target_kind: ArticleKind::Concept,
aliases: vec!["Gcode".to_string(), "gcode".to_string()],
..WikiCompileOptions::default()
},
None,
)
.expect("wiki article compiled");
let page = std::fs::read_to_string(&outcome.article_path).expect("article written");
let parsed = crate::frontmatter::parse_frontmatter(&page).expect("frontmatter parses");
assert_eq!(parsed.metadata.title.as_deref(), Some("Gcode"));
assert_eq!(parsed.metadata.aliases, vec!["gcode"]);
}
#[test]
fn compile_without_generator_stays_structural_without_degradation() {
let temp = tempfile::tempdir().expect("tempdir");
let scope = ResearchScope::project_for_id("project-1", temp.path());
let note_path = scope.root().join("raw/research/compile.md");
std::fs::create_dir_all(note_path.parent().expect("note parent")).expect("raw dir");
std::fs::write(¬e_path, "Accepted compile evidence.").expect("note written");
let mut session = session_with_note(&scope, "Compile behavior", "raw/research/compile.md");
let outcome = compile_to_wiki_with_options(
&mut session,
CompileRequest {
topic: "Structural Compile".to_string(),
outline: vec!["Overview".to_string()],
target_page: None,
write_intent: false,
},
WikiCompileOptions::default(),
None,
)
.expect("wiki article compiled");
let page = std::fs::read_to_string(&outcome.article_path).expect("article written");
assert!(page.contains("synthesis_mode: \"fallback\""), "{page}");
assert!(!page.contains("degraded:"), "{page}");
assert!(page.contains("## Overview"), "{page}");
let report = outcome.explainer.expect("explainer report");
assert_eq!(report.status, "skipped");
}