roder-roadmap 0.1.1

Agentic software development tools and SDKs for Roder.
Documentation
use std::fs;
use std::path::PathBuf;

use roder_roadmap::{
    RoadmapEventKind, RoadmapPromptInput, RoadmapRuntime, parse_document, roadmap_context_prompt,
};

#[test]
fn runtime_opens_focuses_validates_toggles_and_publishes_events() {
    let workspace = temp_workspace();
    let data_dir = workspace.join(".data");
    let path = workspace.join("roadmap/20-roadmapping-mode.md");
    fs::write(&path, fixture()).unwrap();
    let mut runtime = RoadmapRuntime::new(&workspace, &data_dir);

    let document = runtime
        .open_roadmap("roadmap/20-roadmapping-mode.md")
        .unwrap();
    let task_id = document.tasks[0].id.clone();
    runtime
        .focus_roadmap_task("roadmap/20-roadmapping-mode.md", &task_id)
        .unwrap();
    let validation = runtime
        .validate_roadmap("roadmap/20-roadmapping-mode.md")
        .unwrap();
    assert!(validation.diagnostics.is_empty());
    runtime
        .set_roadmap_task(
            "roadmap/20-roadmapping-mode.md",
            &task_id,
            true,
            "unit test evidence",
        )
        .unwrap();
    runtime
        .record_mode_changed("roadmap/20-roadmapping-mode.md")
        .unwrap();

    let updated = fs::read_to_string(&path).unwrap();
    assert!(updated.contains("- [x] Add runtime tests"));
    assert_eq!(
        runtime
            .events()
            .iter()
            .map(|event| event.kind)
            .collect::<Vec<_>>(),
        vec![
            RoadmapEventKind::Opened,
            RoadmapEventKind::TaskFocused,
            RoadmapEventKind::Validated,
            RoadmapEventKind::TaskChecked,
            RoadmapEventKind::ModeChanged,
        ]
    );
}

#[test]
fn runtime_lists_spawns_and_attaches_threads_without_transcript_mutation() {
    let workspace = temp_workspace();
    let data_dir = workspace.join(".data");
    fs::write(workspace.join("roadmap/20-roadmapping-mode.md"), fixture()).unwrap();
    let mut runtime = RoadmapRuntime::new(&workspace, &data_dir);
    let document = runtime
        .open_roadmap("roadmap/20-roadmapping-mode.md")
        .unwrap();
    let task_id = document.tasks[0].id.clone();

    let attached = runtime
        .attach_roadmap_thread(
            "roadmap/20-roadmapping-mode.md",
            &task_id,
            "thread-existing",
            Some("Existing worker".to_string()),
        )
        .unwrap();
    assert_eq!(attached.thread_id, "thread-existing");
    let spawned = runtime
        .spawn_roadmap_thread("roadmap/20-roadmapping-mode.md", &task_id)
        .unwrap();
    assert!(spawned.thread_id.starts_with("thread-"));

    let threads = runtime
        .list_roadmap_threads("roadmap/20-roadmapping-mode.md")
        .unwrap();
    assert_eq!(threads.len(), 2);
    assert!(runtime.events().iter().any(|event| {
        event.kind == RoadmapEventKind::ThreadAttached
            && event.thread_id.as_deref() == Some("thread-existing")
    }));
    assert!(
        runtime
            .events()
            .iter()
            .any(|event| event.kind == RoadmapEventKind::ThreadSpawned)
    );
}

#[test]
fn roadmap_prompt_includes_document_task_validation_and_skill_body() {
    let path = PathBuf::from("roadmap/20-roadmapping-mode.md");
    let document = parse_document(&path, &fixture());
    let validation = roder_roadmap::validate_document(&document);
    let skill_body = fs::read_to_string(
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../.agents/skills/roadmap-planning/SKILL.md"),
    )
    .unwrap();
    let prompt = roadmap_context_prompt(RoadmapPromptInput {
        document: &document,
        focused_task: document.tasks.first(),
        validation: Some(&validation),
        skill_body: Some(&skill_body),
    });

    assert!(prompt.contains("roadmapping mode"));
    assert!(prompt.contains("Orchestrator contract:"));
    assert!(
        prompt
            .contains("spawn one worker per independent unchecked task with roadmap_thread_spawn")
    );
    assert!(prompt.contains("Do not implement tasks in this thread."));
    assert!(prompt.contains("Document: Roadmapping Mode Implementation Plan"));
    assert!(prompt.contains("Focused task: Add runtime tests [open]"));
    assert!(prompt.contains("Validation: no diagnostics."));
    assert!(prompt.contains("Roadmap planning skill:"));
    assert!(prompt.contains("Keep the roadmap document as the source of truth."));
}

fn fixture() -> String {
    "# Roadmapping Mode Implementation Plan\n\n**Goal:** Add a document-first roadmapping mode.\n**Architecture:** Roadmap Markdown documents are primary state.\n**Tech Stack:** Rust.\n\n## Owned Paths\n\n- Create: `crates/roder-roadmap/src/runtime.rs`\n\n## Tasks\n\n- [ ] Add runtime tests\n\nRun:\n\n```sh\ncargo test -p roder-roadmap\n```\n\nAcceptance:\n- Runtime behavior is covered.\n\n## Phase Acceptance\n\n- [ ] Runtime works.\n".to_string()
}

fn temp_workspace() -> PathBuf {
    let path = std::env::temp_dir().join(format!("roadmap-runtime-{}", uuid::Uuid::new_v4()));
    fs::create_dir_all(path.join("roadmap")).unwrap();
    path
}