akashic 0.2.0

Reader-agnostic Markdown knowledge graph engine
Documentation
use assert_cmd::Command;
use predicates::prelude::*;
use std::io::Write;
use tempfile::NamedTempFile;

const VALID_NOTE: &str = r#"---
id: decision-payment-status-events
type: Decision
title: Use event-driven payment status
status: stable
valid_from: "2026-06-01"
scope:
  system: payments
  env: production
relations:
  - type: supersedes
    target: ./decision-payment-status-polling.md
    evidence: ./review-incident-timeout.md
  - type: supported_by
    target: ./review-incident-timeout.md
---
# Architecture Decision

We decide to replace polling with event-driven notifications.
"#;

#[test]
fn test_inspect_valid_note_human() {
    let mut file = NamedTempFile::new().unwrap();
    file.write_all(VALID_NOTE.as_bytes()).unwrap();

    let mut cmd = Command::cargo_bin("akashic").unwrap();
    cmd.arg("inspect")
        .arg(file.path())
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "Node: decision-payment-status-events",
        ))
        .stdout(predicate::str::contains("Type: Decision"))
        .stdout(predicate::str::contains(
            "Title: Use event-driven payment status",
        ))
        .stdout(predicate::str::contains("Status: stable"))
        .stdout(predicate::str::contains("Valid From: 2026-06-01"))
        .stdout(predicate::str::contains("system: payments"))
        .stdout(predicate::str::contains("env: production"))
        .stdout(predicate::str::contains(
            "supersedes -> ./decision-payment-status-polling.md",
        ))
        .stdout(predicate::str::contains(
            "evidence: ./review-incident-timeout.md",
        ))
        .stdout(predicate::str::contains(
            "supported_by -> ./review-incident-timeout.md",
        ));
}

#[test]
fn test_inspect_valid_note_json() {
    let mut file = NamedTempFile::new().unwrap();
    file.write_all(VALID_NOTE.as_bytes()).unwrap();

    let mut cmd = Command::cargo_bin("akashic").unwrap();
    let assert = cmd
        .arg("inspect")
        .arg(file.path())
        .arg("--json")
        .assert()
        .success();

    let output = assert.get_output();
    let stdout_str = String::from_utf8(output.stdout.clone()).unwrap();

    let parsed: serde_json::Value = serde_json::from_str(&stdout_str).expect("Valid JSON");
    assert_eq!(parsed["id"], "decision-payment-status-events");
    assert_eq!(parsed["type"], "Decision");
    assert_eq!(parsed["title"], "Use event-driven payment status");
    assert_eq!(parsed["status"], "stable");
    assert_eq!(parsed["valid_from"], "2026-06-01");
    assert_eq!(parsed["scope"]["system"], "payments");
    assert_eq!(parsed["scope"]["env"], "production");
    assert_eq!(parsed["relations"][0]["type"], "supersedes");
    assert_eq!(
        parsed["relations"][0]["target"],
        "./decision-payment-status-polling.md"
    );
    assert_eq!(
        parsed["relations"][0]["evidence"],
        "./review-incident-timeout.md"
    );
    assert_eq!(parsed["relations"][1]["type"], "supported_by");
    assert!(parsed["body"]
        .as_str()
        .unwrap()
        .contains("# Architecture Decision"));
}

#[test]
fn test_inspect_file_not_found() {
    let mut cmd = Command::cargo_bin("akashic").unwrap();
    cmd.arg("inspect")
        .arg("non_existent_file_path_12345.md")
        .assert()
        .failure()
        .code(predicate::eq(1))
        .stderr(predicate::str::contains("Error:"));
}

#[test]
fn test_inspect_invalid_yaml() {
    let bad_yaml = r#"---
id: [unclosed list
title: broken
---
# Content
"#;
    let mut file = NamedTempFile::new().unwrap();
    file.write_all(bad_yaml.as_bytes()).unwrap();

    let mut cmd = Command::cargo_bin("akashic").unwrap();
    cmd.arg("inspect")
        .arg(file.path())
        .assert()
        .failure()
        .code(predicate::eq(1))
        .stderr(predicate::str::contains("Invalid YAML frontmatter"));
}

#[test]
fn test_inspect_missing_frontmatter() {
    let no_fm = r#"# Regular Markdown

Just some text without any YAML frontmatter.
"#;
    let mut file = NamedTempFile::new().unwrap();
    file.write_all(no_fm.as_bytes()).unwrap();

    let mut cmd = Command::cargo_bin("akashic").unwrap();
    cmd.arg("inspect")
        .arg(file.path())
        .assert()
        .failure()
        .code(predicate::eq(1))
        .stderr(predicate::str::contains(
            "Missing or malformed YAML frontmatter delimiter",
        ));
}

#[test]
fn test_inspect_standard_gfm() {
    let gfm_doc = r#"---
id: concept-gfm-check
type: Concept
title: GFM Compatibility Test
---
# Tables and Lists

| Header 1 | Header 2 |
|----------|----------|
| Val 1    | Val 2    |

- [x] Task list item
- [ ] Incomplete item

~~Strikethrough~~ and `inline code`.

```rust
fn main() {
    println!("Hello, World!");
}
```
"#;
    let mut file = NamedTempFile::new().unwrap();
    file.write_all(gfm_doc.as_bytes()).unwrap();

    let mut cmd = Command::cargo_bin("akashic").unwrap();
    cmd.arg("inspect")
        .arg(file.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("Node: concept-gfm-check"))
        .stdout(predicate::str::contains("Type: Concept"));
}