Arcology Engine

Arroyo Tests — Tangle/Detangle Test Suite

Contents

Tests for the OrgTangle and OrgDetangle classes in the core library. These are pure JVM tests (no Android dependencies) exercising the round-trip between org files and tangled source code. The tests cover flat block extraction, noweb reference resolution (including recursive and circular cases), path normalization, delimiter-based detangling, fallback anchor-based detangling, and noweb-composed file detangling.

Introduction

The test suite validates the two directions of the literate programming pipeline: extracting source code from org files (tangle) and mapping changes back from source files into org blocks (detangle). Because both OrgTangle and OrgDetangle are pure string-in/string-out, the tests construct org content and source content inline as string literals, call the engine, and assert on the result data classes.

Design Decisions

*Inline string literals for test data.*

Each test constructs org content and source content as Kotlin trimIndent() string literals. This keeps tests self-contained and readable — no external test fixture files, no filesystem dependency. The trade-off is that org block indentation must be carefully managed.

*No filesystem mocks.*

Because the core library returns Map<String, String> (tangle) and DetangleResult (detangle) without touching the filesystem, tests can assert directly on the returned data structures. File I/O is tested implicitly through the CLI commands, not through these unit tests.

*Round-trip testing pattern.*

Several tests implicitly validate the round-trip: tangle an org file, then detangle the resulting source file, and verify the detangle output matches the original org blocks. The noweb composed file detangles correctly test is the most explicit round-trip test — it verifies that noweb-composed output (with delimiter comments) can be correctly decomposed back into individual named blocks.

*Coverage of edge cases.*

The tangle tests cover: flat blocks, simple noweb, recursive noweb expansion, circular noweb detection, multiple blocks targeting the same file, :noweb-ref property form, path normalization with ../ from subdirectories, missing noweb refs, and :tangle no skipping. The detangle tests cover: delimiter-based matching, new regions becoming created blocks, stale org blocks, fallback mode with anchor extractors, noweb-composed file detangling, and multiple blocks targeting the same file.

Implementation

TangleTest — tangle engine tests

Nine tests exercising the OrgTangle.tangle() method across its full feature set, plus five tests for Sprint 2 table integration and parameterized noweb, plus shebang and comment-type tests.

kotlin#+name: tangle-test:tangle ../src/commonTest/kotlin/computer/whatthefuck/arroyo/TangleTest.kt
package computer.whatthefuck.arroyo

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

class TangleTest {
    private val tangle = OrgTangle()

    @Test
    fun `flat block extracts to single file`() {
        val orgContent = """
,* A Heading
,#+begin_src kotlin :tangle ../src/File.kt
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue(result.warnings.isEmpty())
        assertEquals(1, result.files.size)
        val content = result.files["src/File.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
        assertTrue(content.contains("[[file:roam/test.org::"))
        assertTrue(content.contains("ends here"))
    }

    @Test
    fun `resolves simple noweb references`() {
        val orgContent = """
,#+name: greeting
,#+begin_src kotlin
"hello"
,#+end_src

,* A Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
val msg = <<greeting>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("\"hello\"") || content.contains("hello"))
    }

    @Test
    fun `noweb ref resolves across different top-level sections`() {
        val orgContent = """
,* Section A
,#+name: shared
,#+begin_src kotlin
shared-body
,#+end_src

,* Section B
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
val msg = <<shared>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue("Expected no MissingNowebRef warnings, got: ${result.warnings}") {
            result.warnings.none { it is TangleWarning.MissingNowebRef }
        }
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("shared-body"))
    }

    @Test
    fun `noweb ref resolves when definition is nested 3 levels deep`() {
        val orgContent = """
,* Section A
,** Subsection B
,*** Subsection C
,#+name: deep
,#+begin_src kotlin
deep-body
,#+end_src

,* Section D
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
val msg = <<deep>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue("Expected no MissingNowebRef warnings, got: ${result.warnings}") {
            result.warnings.none { it is TangleWarning.MissingNowebRef }
        }
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("deep-body"))
    }

    @Test
    fun `bold markup does not swallow source blocks across sections`() {
        // Regression: **bold** markup followed by a paragraph, then a new heading
        // with a source block containing ,* (comma-escaped heading). The re-enabled
        // parseBold in OrgInlineElem would greedily consume across line/section/block
        // boundaries looking for a closing * delimiter, swallowing the heading and
        // source block as paragraph text. The fix: parseEmphasis must stop at
        // structural tokens (HeadingStars, BlockStart/End, EOF, double LineBreak).
        val orgContent = """
,* Design Decisions

,**Bold text.**

Some prose here.

,* Implementation

,#+name: tangle-test
,#+begin_src kotlin :tangle ../src/Out.kt
val x = 1
,* A Heading
val y = 2
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue("Expected no MissingNowebRef warnings, got: ${result.warnings}") {
            result.warnings.none { it is TangleWarning.MissingNowebRef }
        }
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
        assertTrue(content.contains("val y = 2"))
    }

    @Test
    fun `multiline noweb preserves line prefix`() {
        val orgContent = """
,#+name: body
,#+begin_src kotlin
hello
world
,#+end_src

,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
source:
,# <<body>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("source:"))
        assertTrue(content.contains("# hello"))
        assertTrue(content.contains("# world"))
    }

    @Test
    fun `handles recursive noweb expansion`() {
        val orgContent = """
,#+name: B
,#+begin_src kotlin
bContent
,#+end_src

,#+name: A
,#+begin_src kotlin
<<B>>-aContent
,#+end_src

,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
result: <<A>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("bContent-aContent"))
    }

    @Test
    fun `detects circular noweb references`() {
        val orgContent = """
,#+name: A
,#+begin_src kotlin
<<B>>
,#+end_src

,#+name: B
,#+begin_src kotlin
<<A>>
,#+end_src

,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
<<A>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue(result.warnings.any { it is TangleWarning.CircularNowebRef })
    }

    @Test
    fun `multiple blocks concatenate to same target`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt
val a = 1
,#+end_src

Some text here.

,#+begin_src kotlin :tangle ../src/Out.kt
val b = 2
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val a = 1"))
        assertTrue(content.contains("val b = 2"))
    }

    @Test
    fun `noweb-ref property form`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :noweb-ref phase0
val x = 1
,#+end_src

,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
<<phase0>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
    }

    @Test
    fun `multiple noweb-ref blocks concatenate in document order`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :noweb-ref combined
val a = 1
,#+end_src

,#+begin_src kotlin :noweb-ref combined
val b = 2
,#+end_src

,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
<<combined>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        val aIdx = content.indexOf("val a = 1")
        val bIdx = content.indexOf("val b = 2")
        assertTrue(aIdx >= 0, "val a = 1 should be present")
        assertTrue(bIdx >= 0, "val b = 2 should be present")
        assertTrue(aIdx < bIdx, "val a = 1 should come before val b = 2")
    }

    @Test
    fun `multiple NAME blocks with same name concatenate`() {
        val orgContent = """
,#+name: combined
,#+begin_src kotlin
val a = 1
,#+end_src

,#+name: combined
,#+begin_src kotlin
val b = 2
,#+end_src

,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
<<combined>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        val aIdx = content.indexOf("val a = 1")
        val bIdx = content.indexOf("val b = 2")
        assertTrue(aIdx >= 0, "val a = 1 should be present")
        assertTrue(bIdx >= 0, "val b = 2 should be present")
        assertTrue(aIdx < bIdx, "val a = 1 should come before val b = 2")
    }

    @Test
    fun `path resolution with dot dot slash from subdirectory`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/commonMain/File.kt
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "subdir/test.org")
        val content = result.files["src/commonMain/File.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
    }

    @Test
    fun `missing noweb ref generates warning`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
<<nonexistent>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue(result.warnings.any { it is TangleWarning.MissingNowebRef })
    }

    @Test
    fun `blocks with tangle no are skipped`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle no
val x = 1
,#+end_src

,#+begin_src kotlin :tangle ../src/Out.kt
val y = 2
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertEquals(1, result.files.size)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val y = 2"))
        assertTrue(!content.contains("val x = 1"))
    }

    @Test
    fun `blocks with comments none are not wrapped`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :comments none
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertEquals(1, result.files.size)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
        assertTrue(!content.contains("ends here"))
        assertTrue(!content.contains("[[file:"))
    }

    @Test
    fun `blocks with comments no are not wrapped`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :comments no
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertEquals(1, result.files.size)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
        assertTrue(!content.contains("ends here"))
        assertTrue(!content.contains("[[file:"))
    }

    @Test
    fun `heading id delimiter is closed for non-symmetric comment styles`() {
        val orgContent = """
,* Heading
:PROPERTIES:
:ID: 20260826T193523.999999
:END:
,#+name: my-css
,#+begin_src css :tangle ../src/Out.css
.foo { color: red; }
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertEquals(1, result.files.size)
        val content = result.files["src/Out.css"] ?: error("Missing file")
        assertTrue(content.contains(".foo { color: red; }"))
        assertTrue(content.contains("/* [[file:"))
        assertTrue(content.contains("/* [[id:20260826T193523.999999]] */"))
        assertTrue(content.contains("/* my-css ends here */"))
    }

    @Test
    fun `blocks without comments arg are wrapped by default`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertEquals(1, result.files.size)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
        assertTrue(content.contains("ends here"))
        assertTrue(content.contains("[[file:"))
    }

    @Test
    fun `named table is collected and available for var binding`() {
        val orgContent = """
,#+name: packages
| name    | pkg              | description |
|---------+------------------+-------------|
| firefox | firefox-bin      | web browser |
| emacs   | emacs-unstable   | text editor |
| alacritty | alacritty      | terminal    |

,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes :var tbl=packages
<<table-dumper>>
,#+end_src

,#+name: table-dumper
,#+begin_src kotlin
val count = 3
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertEquals(1, result.files.size)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val count = 3"))
    }

    @Test
    fun `table with no name is not collected`() {
        val orgContent = """
,* Heading
| name    | pkg         |
|---------+-------------|
| firefox | firefox-bin |

,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes :var tbl=packages
<<table-dumper>>
,#+end_src

,#+name: table-dumper
,#+begin_src kotlin
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
    }

    @Test
    fun `parameterized noweb resolves ref with arg`() {
        val orgContent = """
,#+name: rix-pkgs
| name      | pkg      |
|-----------+----------|
| signald   | signald  |

,#+name: gen-overlay
,#+begin_src nix
generated overlay for table
,#+end_src

,* Heading
,#+begin_src nix :tangle ../flake.nix :noweb yes
<<gen-overlay(rix-pkgs)>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["flake.nix"] ?: error("Missing file")
        assertTrue(content.contains("generated overlay for table"))
    }

    @Test
    fun `parameterized noweb with unresolved table emits warning`() {
        val orgContent = """
,#+name: gen-overlay
,#+begin_src nix
generated overlay for table
,#+end_src

,* Heading
,#+begin_src nix :tangle ../flake.nix :noweb yes
<<gen-overlay(nonexistent)>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue(result.warnings.any { it is TangleWarning.UnresolvedTableRef })
        val content = result.files["flake.nix"] ?: error("Missing file")
        assertTrue(content.contains("generated overlay for table"))
    }

    @Test
    fun `table toNestedList strips org link syntax from cells`() {
        val orgContent = """
,#+name: link-table
| name    | link                               |
|---------+------------------------------------|
| firefox | [[https://firefox.com][Firefox]] |
| emacs   | [[https://gnu.org/emacs][GNU Emacs]] |

,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes :var tbl=link-table
<<table-dumper>>
,#+end_src

,#+name: table-dumper
,#+begin_src kotlin
val x = 1
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("val x = 1"))
    }

@Test
    fun `resolveNoweb still works with plain ref without args`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
<<greeting>>
,#+end_src

,#+name: greeting
,#+begin_src kotlin
hello
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("hello"))
    }

    @Test
    fun `eval arroyo block with mock context uses evaluated output`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                return "echo " + script.trim()
            }
        }
        val orgContent = """
,* Heading
,#+begin_src lua :tangle ../src/Out.kt :eval arroyo
hello from lua
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("echo hello from lua"))
    }

    @Test
    fun `eval arroyo block with null context emits warning`() {
        val orgContent = """
,* Heading
,#+begin_src lua :tangle ../src/Out.kt :eval arroyo
hello
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        assertTrue(result.warnings.any { it is TangleWarning.MissingEvalContext })
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("hello"))
    }

    @Test
    fun `eval error throws TangleEvalException and writes no file`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                throw RuntimeException("boom")
            }
        }
        val orgContent = """
,* Heading
,#+begin_src lua :tangle ../src/Out.kt :eval arroyo
crash
,#+end_src
        """.trimIndent()

        val ex = assertFailsWith<TangleEvalException> {
            tangle.tangle(orgContent, "roam/test.org", ctx)
        }
        assertTrue(ex.message!!.contains("boom"))
    }

    @Test
    fun `eval error in parameterized noweb ref throws TangleEvalException`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                throw RuntimeException("kaboom")
            }
        }
        val orgContent = """
,#+name: rix-pkgs
| name    | pkg    |
|---------+--------|
| signald | signald |

,#+name: crash-ref
,#+begin_src lua :eval arroyo :var tbl=rix-pkgs
return "should not get here"
,#+end_src

,* Heading
,#+begin_src nix :tangle ../flake.nix :noweb yes
<<crash-ref(rix-pkgs)>>
,#+end_src
        """.trimIndent()

        val ex = assertFailsWith<TangleEvalException> {
            tangle.tangle(orgContent, "roam/test.org", ctx)
        }
        assertTrue(ex.blockName == "crash-ref")
        assertTrue(ex.message!!.contains("kaboom"))
    }

    @Test
    fun `eval error in one of two blocks targeting same file throws and writes nothing`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                throw RuntimeException("nope")
            }
        }
        val orgContent = """
,* Heading
,#+begin_src python :tangle ../src/Out.kt
val ok = 1
,#+end_src

,#+begin_src lua :tangle ../src/Out.kt :eval arroyo
crash
,#+end_src
        """.trimIndent()

        assertFailsWith<TangleEvalException> {
            tangle.tangle(orgContent, "roam/test.org", ctx)
        }
    }

    @Test
    fun `eval arroyo with var binding passes table data to eval`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                val rows = varBindings["tbl"] ?: return "no data"
                return rows.joinToString(",") { it.joinToString(":") }
            }
        }
        val orgContent = """
,#+name: my-data
| name | value |
|------+-------|
| foo  | 1     |
| bar  | 2     |

,* Heading
,#+begin_src lua :tangle ../src/Out.kt :eval arroyo :var tbl=my-data
use the table
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("foo:1,bar:2"))
    }

    @Test
    fun `comments none on eval block skips delimiter wrapping`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                return "generated content"
            }
        }
        val orgContent = """
,* Heading
,#+begin_src nix :tangle ../result.nix :eval arroyo :comments none
generate something
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["result.nix"] ?: error("Missing file")
        assertTrue(content.contains("generated content"))
        assertTrue(!content.contains("ends here"))
        assertTrue(!content.contains("[[file:"))
    }

    @Test
    fun `results raw is not a tangle knob and does not skip wrapping`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                return "generated content"
            }
        }
        val orgContent = """
,* Heading
,#+begin_src nix :tangle ../result.nix :eval arroyo :results raw
generate something
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["result.nix"] ?: error("Missing file")
        assertTrue(content.contains("generated content"))
        assertTrue(content.contains("ends here"))
        assertTrue(content.contains("[[file:"))
    }

    @Test
    fun `parameterized ref to eval block passes arg table`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                val rows = varBindings["tbl"] ?: return "no binding"
                return "generated " + rows.joinToString(",") { it.joinToString(":") }
            }
        }
        val orgContent = """
,#+name: rix-pkgs
| name      | pkg       |
|-----------+-----------|
| signald   | signald   |

,#+name: gen-overlay
,#+begin_src nix :var tbl=rix-pkgs :eval arroyo
use the table
,#+end_src

,* Heading
,#+begin_src nix :tangle ../flake.nix :noweb yes
<<gen-overlay(rix-pkgs)>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["flake.nix"] ?: error("Missing file")
        assertTrue(content.contains("generated signald:signald"))
    }

    @Test
    fun `cross-file table ref resolves and passes data to eval`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                val rows = varBindings["tbl"] ?: return "no data"
                return "hosts=" + rows.joinToString(";") { it.joinToString(":") }
            }
        }
        val externalOrg = """
,#+name: hosts
| hostname  | port |
|-----------+------|
| example.com | 443  |
| api.example.com | 8080  |
        """.trimIndent()
        val fileReader: (String) -> String = { externalOrg }
        val orgContent = """
,* Heading
,#+begin_src nix :tangle ../result.nix :eval arroyo :var tbl=other.org:hosts
generate hosts
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx, fileReader)
        val content = result.files["result.nix"] ?: error("Missing file")
        assertTrue(content.contains("hosts=example.com:443;api.example.com:8080"))
    }

    @Test
    fun `cross-file table ref with missing file emits UnresolvedTableRef warning`() {
        val fileReader: (String) -> String = { throw Exception("no such file") }
        val orgContent = """
,* Heading
,#+begin_src nix :tangle ../result.nix :eval arroyo :var tbl=nonexistent.org:hosts
generate hosts
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", null, fileReader)
        assertTrue(result.warnings.any { it is TangleWarning.UnresolvedTableRef })
    }

    @Test
    fun `quoted string var binding passes string to eval`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                val role = stringBindings["role"] ?: return "no role"
                return "role=" + role
            }
        }
        val orgContent = """
,* Heading
,#+begin_src lua :tangle ../src/Out.kt :eval arroyo :var role="endpoint"
generate
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["src/Out.kt"] ?: error("Missing file")
        assertTrue(content.contains("role=endpoint"))
    }

    @Test
    fun `parameterized ref with quoted string arg passes string to eval`() {
        val ctx = object : ArroyoTangleContext {
            override fun nixosModules(role: String?) = emptyList<String>()
            override fun nixosModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun homeModules(role: String?) = emptyList<String>()
            override fun homeModulesMeta(role: String?) = emptyList<ModuleMeta>()
            override fun emacsSnippets() = emptyList<String>()
            override fun emacsInit() = ""
            override fun emacsModulesMeta() = emptyList<EmacsModuleMeta>()
            override fun epkgOverrides() = emptyList<String>()
            override fun fileTitleFor(filePath: String) = null
            override fun tableFor(name: String): List<List<String>> = emptyList<List<String>>()
            override fun flakeInputs(orgDir: String) = emptyList<FlakeInput>()
            override fun nixosRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun homeRoleModules(role: String, orgDir: String) = emptyList<FlakeModuleRef>()
            override fun systemOverlays(role: String?) = emptyList<String>()
            override fun inputs(role: String?) = emptyList<String>()
            override fun outputs() = emptyList<String>()
            override fun ageRecipients(role: String?) = emptyList<AgeRecipient>()
            override fun evalBlock(script: String, varBindings: Map<String, List<List<String>>>, stringBindings: Map<String, String>): String {
                val role = stringBindings["role"] ?: return "no role"
                return "role=" + role
            }
        }
        val orgContent = """
,#+name: gen-role
,#+begin_src lua :var role="endpoint" :eval arroyo
generate
,#+end_src

,* Heading
,#+begin_src nix :tangle ../flake.nix :noweb yes
<<gen-role("server")>>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org", ctx)
        val content = result.files["flake.nix"] ?: error("Missing file")
        assertTrue(content.contains("role=server"))
    }

    @Test
    fun `shebang t uses default interpreter for language`() {
        val orgContent = """
,* Heading
,#+begin_src bash :tangle ../scripts/run.sh :shebang t
echo hello
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["scripts/run.sh"] ?: error("Missing file")
        assertTrue(content.startsWith("#!/usr/bin/env bash\n"))
        assertTrue(content.contains("echo hello"))
        assertTrue(result.executableFiles.contains("scripts/run.sh"))
    }

    @Test
    fun `shebang with explicit interpreter is prepended`() {
        val orgContent = """
,* Heading
,#+begin_src python :tangle ../scripts/foo.py :shebang "#!/snap/bin/current-python"
print("hi")
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["scripts/foo.py"] ?: error("Missing file")
        assertTrue(content.startsWith("#!/snap/bin/current-python\n"))
        assertTrue(content.contains("print(\"hi\")"))
        assertTrue(result.executableFiles.contains("scripts/foo.py"))
    }

    @Test
    fun `shebang no does not add shebang or mark executable`() {
        val orgContent = """
,* Heading
,#+begin_src bash :tangle ../scripts/skip.sh :shebang no
echo hello
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["scripts/skip.sh"] ?: error("Missing file")
        assertTrue(!content.startsWith("#!"))
        assertTrue(!result.executableFiles.contains("scripts/skip.sh"))
    }

    @Test
    fun `shebang only applied once for multi-block file`() {
        val orgContent = """
,* Heading
,#+begin_src bash :tangle ../scripts/once.sh :shebang t
echo first
,#+end_src

,#+begin_src bash :tangle ../scripts/once.sh
echo second
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["scripts/once.sh"] ?: error("Missing file")
        val shebangCount = Regex("^#!.*$", RegexOption.MULTILINE).findAll(content).count()
        assertEquals(1, shebangCount)
        assertTrue(content.contains("echo first"))
        assertTrue(content.contains("echo second"))
        assertTrue(result.executableFiles.contains("scripts/once.sh"))
    }

    @Test
    fun `shell comment prefix uses hash`() {
        val orgContent = """
,* Heading
,#+begin_src shell :tangle ../scripts/s.sh
echo hi
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["scripts/s.sh"] ?: error("Missing file")
        assertTrue(content.contains("# [[file:roam/test.org::"))
        assertTrue(content.contains("# ") && content.contains("ends here"))
    }

    @Test
    fun `yaml comment prefix uses hash`() {
        val orgContent = """
,* Heading
,#+begin_src yaml :tangle ../config/app.yaml
key: value
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["config/app.yaml"] ?: error("Missing file")
        assertTrue(content.contains("# [[file:roam/test.org::"))
        assertTrue(content.contains("# ") && content.contains("ends here"))
    }

    @Test
    fun `toml comment prefix uses hash`() {
        val orgContent = """
,* Heading
,#+begin_src toml :tangle ../config/app.toml
[section]
key = "value"
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["config/app.toml"] ?: error("Missing file")
        assertTrue(content.contains("# [[file:roam/test.org::"))
    }

    @Test
    fun `html comment prefix uses angle bracket syntax`() {
        val orgContent = """
,* Heading
,#+begin_src html :tangle ../web/index.html
<div>hello</div>
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["web/index.html"] ?: error("Missing file")
        assertTrue(content.contains("<!-- [[file:roam/test.org::"))
        assertTrue(content.contains("<!-- ") && content.contains("ends here -->"))
    }

    @Test
    fun `css comment prefix uses slash-star syntax`() {
        val orgContent = """
,* Heading
,#+begin_src css :tangle ../web/style.css
body { margin: 0; }
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["web/style.css"] ?: error("Missing file")
        assertTrue(content.contains("/* [[file:roam/test.org::"))
        assertTrue(content.contains("ends here */"))
    }

    @Test
    fun `jinja comment prefix uses hash`() {
        val orgContent = """
,* Heading
,#+begin_src jinja :tangle ../templates/t.j2
{{ x }}
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["templates/t.j2"] ?: error("Missing file")
        assertTrue(content.contains("# [[file:roam/test.org::"))
    }

    @Test
    fun `lua comment prefix uses double dash`() {
        val orgContent = """
,* Heading
,#+begin_src lua :tangle ../scripts/s.lua
print("hi")
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["scripts/s.lua"] ?: error("Missing file")
        assertTrue(content.contains("-- [[file:roam/test.org::"))
        assertTrue(content.contains("-- ") && content.contains("ends here"))
    }

    @Test
    fun `vim comment prefix uses double quote`() {
        val orgContent = """
,* Heading
,#+begin_src vim :tangle ../vim/v.vim
set nocompatible
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["vim/v.vim"] ?: error("Missing file")
        assertTrue(content.contains("\" [[file:roam/test.org::"))
        assertTrue(content.contains("\" ") && content.contains("ends here"))
    }

    @Test
    fun `unknown language falls back to slash comment`() {
        val orgContent = """
,* Heading
,#+begin_src brainfuck :tangle ../weird.bf
++++++++++
,#+end_src
        """.trimIndent()

        val result = tangle.tangle(orgContent, "roam/test.org")
        val content = result.files["weird.bf"] ?: error("Missing file")
        assertTrue(content.contains("// [[file:roam/test.org::"))
    }

    @Test
    fun `default shebang returns null for non scripting language`() {
        assertEquals(null, defaultShebangFor("kotlin"))
        assertEquals(null, defaultShebangFor("html"))
        assertEquals("#!/usr/bin/env bash", defaultShebangFor("bash"))
        assertEquals("#!/usr/bin/env python3", defaultShebangFor("python"))
    }
}

DetangleTest — detangle engine tests

Six tests exercising the OrgDetangle.detangle() method across its two modes (delimited and fallback) and edge cases.

The primary tests validate the delimiter-based mode: existing block matches delimiter verifies that a block body is correctly extracted and matched to its org block; new delimiter region without org block generates a created suggestion; org block without matching delimiter marks the block as stale.

The noweb composed file detangles correctly test validates the round-trip for noweb-composed files — it sets up a preamble block, a body block that references a method block via noweb, tangles everything into a combined source file, then simulates the detangle and verifies both preamble and body are individually matched. The multiple blocks targeting same file test verifies that blocks targeting the same tangle path are all individually matched.

The fallback mode test verifies that when no delimiters are present, the Kotlin anchor extractor identifies fun myFun as a strong anchor and the function body becomes a created suggestion.

kotlin#+name: detangle-test:tangle ../src/commonTest/kotlin/computer/whatthefuck/arroyo/DetangleTest.kt
package computer.whatthefuck.arroyo

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFalse

class DetangleTest {
    private val detangle = OrgDetangle()

    @Test
    fun `existing block matches delimiter and body is updated`() {
        val orgContent = """
,* Heading
,#+name: myBlock
,#+begin_src kotlin :tangle ../src/Out.kt
old content
,#+end_src
        """.trimIndent()

        val sourceContent = """
// [[file:roam/test.org::myBlock][myBlock]]
new content
// myBlock ends here
        """.trimIndent()

        val result = detangle.detangle(sourceContent, orgContent, "../src/Out.kt")
        assertEquals(1, result.matched.size)
        assertEquals("myBlock", result.matched[0].blockName)
        assertEquals("new content", result.matched[0].newContent)
        assertEquals(0, result.created.size)
        assertEquals(0, result.stale.size)
    }

    @Test
    fun `new delimiter region without org block creates new block`() {
        val orgContent = """
,* Heading
,#+begin_src kotlin :tangle ../src/Out.kt
val x = 1
,#+end_src
        """.trimIndent()

        val sourceContent = """
// [[file:roam/test.org::myBlock][myBlock]]
new content
// myBlock ends here
        """.trimIndent()

        val result = detangle.detangle(sourceContent, orgContent, "../src/Out.kt")
        assertEquals(0, result.matched.size)
        assertEquals(1, result.created.size)
        assertEquals("myBlock", result.created[0].suggestedName)
    }

    @Test
    fun `org block without matching delimiter is marked stale`() {
        val orgContent = """
,* Heading
,#+name: missingBlock
,#+begin_src kotlin :tangle ../src/Out.kt
content
,#+end_src
        """.trimIndent()

        val sourceContent = "// Nothing here\n"

        val result = detangle.detangle(sourceContent, orgContent, "../src/Out.kt")
        assertTrue(result.stale.isNotEmpty())
        assertEquals("missingBlock", result.stale[0].blockName)
    }

    @Test
    fun `fallback mode with no delimiters uses anchor registry`() {
        val orgContent = """
,* Heading
,#+name: myFun
,#+begin_src kotlin :tangle ../src/Out.kt
fun myFun() { }
,#+end_src
        """.trimIndent()

        val sourceContent = """
package foo

fun myFun(): String = "hello"
        """.trimIndent()

        val result = detangle.detangle(sourceContent, orgContent, "../src/Out.kt")
        assertTrue(result.isFallback)
        assertTrue(result.created.any { it.suggestedName == "myFun" })
    }

    @Test
    fun `noweb composed file detangles correctly`() {
        val orgContent = """
,* Heading
,#+name: preamble
,#+begin_src kotlin :tangle ../src/Out.kt
package foo
,#+end_src

,#+name: body
,#+begin_src kotlin :tangle ../src/Out.kt :noweb yes
class Foo {
    <<method>>
}
,#+end_src

,#+name: method
,#+begin_src kotlin
fun bar() = 1
,#+end_src
        """.trimIndent()

        val sourceContent = """
// [[file:roam/test.org::preamble][preamble]]
package foo
// preamble ends here
// [[file:roam/test.org::body][body]]
class Foo {
fun bar() = 1
}
// body ends here
        """.trimIndent()

        val result = detangle.detangle(sourceContent, orgContent, "../src/Out.kt")
        assertEquals(2, result.matched.size)
        assertTrue(result.matched.any { it.blockName == "preamble" })
        assertTrue(result.matched.any { it.blockName == "body" })
    }

    @Test
    fun `multiple blocks targeting same file`() {
        val orgContent = """
,* Heading
,#+name: blockA
,#+begin_src kotlin :tangle ../src/Out.kt
val a = 1
,#+end_src

,#+name: blockB
,#+begin_src kotlin :tangle ../src/Out.kt
val b = 2
,#+end_src
        """.trimIndent()

        val sourceContent = """
// [[file:roam/test.org::blockA][blockA]]
val a = 1
// blockA ends here
// [[file:roam/test.org::blockB][blockB]]
val b = 2
// blockB ends here
        """.trimIndent()

        val result = detangle.detangle(sourceContent, orgContent, "../src/Out.kt")
        assertEquals(2, result.matched.size)
        assertTrue(result.matched.any { it.blockName == "blockA" })
        assertTrue(result.matched.any { it.blockName == "blockB" })
    }
}

FloodCommandTest — flood pipeline tests

Tests for the FloodCommand and matchesRole utility. These are pure JVM tests (no filesystem I/O) that verify role matching logic and the flood pipeline orchestration.

kotlin#+name: flood-command-test:tangle ../src/jvmTest/kotlin/computer/whatthefuck/arroyo/FloodCommandTest.kt
package computer.whatthefuck.arroyo

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

class FloodCommandTest {

    @Test
    fun `role filter matches space-separated tokens`() {
        val value = "endpoint server droid"
        assertTrue(matchesRole(value, "endpoint"))
        assertTrue(matchesRole(value, "server"))
        assertTrue(matchesRole(value, "droid"))
        assertFalse(matchesRole(value, "mobile"))
        assertFalse(matchesRole(value, "edge"))
    }

    @Test
    fun `bare yes matches any role`() {
        val value = "yes"
        assertTrue(matchesRole(value, "endpoint"))
        assertTrue(matchesRole(value, "server"))
        assertTrue(matchesRole(value, "droid"))
        assertTrue(matchesRole(value, "waterboy"))
    }

    @Test
    fun `single role value matches exactly`() {
        val value = "endpoint"
        assertTrue(matchesRole(value, "endpoint"))
        assertFalse(matchesRole(value, "server"))
        assertFalse(matchesRole(value, "endpoint server"))
    }

    @Test
    fun `role filter is case sensitive`() {
        val value = "Endpoint"
        assertTrue(matchesRole(value, "Endpoint"))
        assertFalse(matchesRole(value, "endpoint"))
    }

    @Test
    fun `leading and trailing whitespace is handled`() {
        val value = "  endpoint  server  "
        assertTrue(matchesRole(value, "endpoint"))
        assertTrue(matchesRole(value, "server"))
        assertFalse(matchesRole(value, "droid"))
    }

    @Test
    fun `yes with other roles still matches`() {
        val value = "yes endpoint"
        assertTrue(matchesRole(value, "endpoint"))
        assertTrue(matchesRole(value, "server"))
        assertTrue(matchesRole(value, "droid"))
    }

    @Test
    fun `rebuild options valid when both present`() {
        assertNull(validateRebuildOptions("switch", listOf("myhost")))
    }

    @Test
    fun `rebuild options valid when both absent`() {
        assertNull(validateRebuildOptions(null, emptyList()))
    }

    @Test
    fun `rebuild options valid with multiple hosts`() {
        assertNull(validateRebuildOptions("switch", listOf("host1", "host2")))
    }

    @Test
    fun `rebuild options invalid when only action present`() {
        assertEquals("--rebuild requires at least one --on", validateRebuildOptions("switch", emptyList()))
    }

    @Test
    fun `rebuild options invalid when only host present`() {
        assertEquals("--on requires --rebuild", validateRebuildOptions(null, listOf("myhost")))
    }
}

Related Modules