gobby-wiki 0.3.0

Gobby wiki CLI shell
use super::render::{canonicalize_location, existing_index_without_manifest};
use super::*;
use crate::IngestFileOptions;
use gobby_core::config::AiRouting;
use std::path::PathBuf;

#[test]
fn dedupes_by_canonical_identity_and_hash() {
    let temp = tempfile::tempdir().expect("tempdir");
    let content = b"Source material about durable wiki provenance.".to_vec();

    let first = SourceManifest::register(
        temp.path(),
        SourceDraft::url(
            "https://Example.com/docs/provenance#chunk-1",
            "2026-05-29T15:00:00Z",
            content.clone(),
        )
        .with_title("Durable provenance")
        .with_citation("Example Docs, Durable provenance")
        .with_license("Apache-2.0"),
    )
    .expect("first source registered");
    let duplicate = SourceManifest::register(
        temp.path(),
        SourceDraft::url(
            "https://example.com/docs/provenance/",
            "2026-05-29T16:00:00Z",
            content,
        )
        .with_title("Duplicate durable provenance")
        .with_citation("Duplicate citation should not replace record")
        .with_license("MIT"),
    )
    .expect("duplicate source reused");

    assert_eq!(duplicate, first);

    let index = std::fs::read_to_string(SourceManifest::index_path(temp.path()))
        .expect("raw index written");
    assert_eq!(index.matches("gwiki-source:").count(), 1);
    assert!(index.contains("https://Example.com/docs/provenance#chunk-1"));
    assert!(index.contains("kind: `url`"));
    assert!(index.contains("fetched_at: `2026-05-29T15:00:00Z`"));
    assert!(index.contains(&first.content_hash));
    assert!(index.contains("citation: Example Docs, Durable provenance"));
    assert!(index.contains("license: Apache-2.0"));
    assert!(index.contains("ingestion_method: `manual`"));
    assert!(index.contains("compile_status: `pending`"));
}

#[test]
fn local_file_replay_metadata_round_trips_through_manifest() {
    let temp = tempfile::tempdir().expect("tempdir");
    let record = SourceManifest::register(
        temp.path(),
        SourceDraft {
            location: "notes/source.md".to_string(),
            kind: SourceKind::Markdown,
            fetched_at: "2026-06-02T00:00:00Z".to_string(),
            content: b"# Source\n".to_vec(),
            title: Some("Source".to_string()),
            citation: Some("notes/source.md".to_string()),
            license: None,
            ingestion_method: IngestionMethod::Manual,
            compile_status: CompileStatus::Pending,
        },
    )
    .expect("register source");
    let options = IngestFileOptions {
        no_ai: true,
        translate: true,
        target_lang: Some("es".to_string()),
        video_frame_interval_seconds: Some(11),
        transcription_routing: Some(AiRouting::Direct),
        vision_routing: Some(AiRouting::Off),
        text_routing: Some(AiRouting::Daemon),
    };
    let replay = SourceReplay::local_file(PathBuf::from("notes/source.md"), &options);

    SourceManifest::update(temp.path(), |manifest| {
        manifest.entries[0].replay = Some(replay);
        Ok(true)
    })
    .expect("write replay metadata");

    let manifest = SourceManifest::read(temp.path()).expect("read source manifest");
    assert_eq!(manifest.entries.len(), 1);
    assert_eq!(manifest.entries[0].id, record.id);
    let Some(SourceReplay::LocalFile {
        path,
        options: replay_options,
    }) = &manifest.entries[0].replay
    else {
        panic!("expected local file replay");
    };
    assert_eq!(path, &PathBuf::from("notes/source.md"));
    assert_eq!(
        replay_options.transcription_routing.as_deref(),
        Some("direct")
    );
    assert_eq!(replay_options.vision_routing.as_deref(), Some("off"));
    assert_eq!(replay_options.text_routing.as_deref(), Some("daemon"));

    let restored = replay_options
        .to_ingest_file_options()
        .expect("replay options parse");
    assert_eq!(restored, options);
    let index = std::fs::read_to_string(SourceManifest::index_path(temp.path()))
        .expect("raw index written");
    assert!(index.contains("\"replay\""));
    assert!(index.contains("\"kind\":\"local_file\""));
}

#[test]
fn canonical_location_sorts_query_before_trimming_slash() {
    assert_eq!(
        canonicalize_location("https://Example.com/docs/?b=2&a=1#frag"),
        "https://example.com/docs?a=1&b=2"
    );
}

#[test]
fn existing_index_strips_unmarked_manifest_until_next_heading() {
    let temp = tempfile::tempdir().expect("tempdir");
    let index_path = SourceManifest::index_path(temp.path());
    std::fs::create_dir_all(index_path.parent().expect("raw dir")).expect("raw dir");
    std::fs::write(
        &index_path,
        "# Raw Sources\n\nManual note.\n\n## Source manifest\n\n- generated\n\n## Generated Heading\n\n- stale generated content\n",
    )
    .expect("index");

    let stripped = existing_index_without_manifest(&index_path).expect("strip manifest");

    assert!(stripped.prefix.contains("Manual note."));
    assert!(!stripped.prefix.contains("Generated Heading"));
    assert!(stripped.suffix.contains("Generated Heading"));
    assert!(stripped.suffix.contains("stale generated content"));
}

#[test]
fn existing_index_preserves_content_after_marked_manifest() {
    let temp = tempfile::tempdir().expect("tempdir");
    let index_path = SourceManifest::index_path(temp.path());
    std::fs::create_dir_all(index_path.parent().expect("raw dir")).expect("raw dir");
    std::fs::write(
        &index_path,
        format!(
            "# Raw Sources\n\nManual before.\n\n{GENERATED_SOURCE_MANIFEST_START}\n## Source manifest\n\n- generated\n{GENERATED_SOURCE_MANIFEST_END}\n\nManual after.\n",
        ),
    )
    .expect("index");

    let stripped = existing_index_without_manifest(&index_path).expect("strip manifest");

    assert!(stripped.prefix.contains("Manual before."));
    assert!(!stripped.prefix.contains("generated"));
    assert!(stripped.suffix.contains("Manual after."));
}