ContextResolutionLedgerSmokeTest.kt
src/jvmMain/kotlin/afield/examples/ContextResolutionLedgerSmokeTest.kt · 220 lines
package afield.examples
import afield.document.graph.Box
import afield.tools.automation.OperationTranscript
import afield.document.persistence.SavingContext
import afield.app.server.SessionInformation
import afield.app.server.thisSessionInfo
import java.io.File
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import kotlin.system.exitProcess
FIELD_TEST id: examples.context.resolution-ledger runner: jvm-main entrypoint: afield.examples.ContextResolutionLedgerSmokeTest tags: [smoke, context, exports, language, regression] timeoutSec: 300 expect: exitCode: 0 stdoutContains: - "ContextResolutionLedgerSmokeTest ok"
object ContextResolutionLedgerSmokeTest
The live-engine half of ContextResolutionCharacterizationTest: the two resolution behaviours
that only exist once a real box has run — a variable it really left behind, and a preamble line
checkPreambleAndExecute really persisted.
@exportedself-pinning. A consumer that writesval x = _xleaves a variable calledxin its own table, andInheritanceFromContextlooks forxas well as_x. Does the consumer's second run see the exporter's new value, or its own leftover?- A persisted inherited preamble line after a disconnect. The consumer's preamble gets the exporter's inherit-marked declaration written into it. After the edge goes, is it still there, and does the consumer's own child still inherit it?
LEDGER is read in both directions, like ContextSearchOrderLedgerSmokeTest's. Its lines are
dated: each records what the engine did on that date, and a line that was flipped deliberately
names the ledger entry in dev_docs/stdlib_behaviour_changes.md that explains it.
object ContextResolutionLedgerSmokeTest {
2026-09-28, before ContextResolution: the consumer pinned itself to its first answer
(pass2 x=1), the stale declaration survived the disconnect in the consumer, and the
consumer's child inherited it.
FLIPPED 2026-09-29 (stdlib_behaviour_changes.md, "one context walk"): pass2 x=2 — the
consumer's own leftover x no longer answers for _x.
FLIPPED 2026-09-29 (stdlib_behaviour_changes.md, "sourced preamble lines"): the persisted declaration carries its supplier's uid, so after the disconnect it leaves the consumer's preamble and the grandchild does not inherit it.
source — 80 lines, from line 55
private val LEDGER = listOf(
"ledger: consumer pass1 x=1",
"ledger: consumer pass2 x=2",
"ledger: exporter own=2",
"ledger: consumer after-disconnect stale-line=false",
"ledger: grandchild after-disconnect inherits=false",
)
private const val EXPORTER_V1 = """@exported
var x = 1
"""
private const val EXPORTER_V2 = """@exported
var x = 2
"""
private const val EXPORTER_READS_OWN = """println("ledger: exporter own=" + _x)
"""
private fun consumer(label: String) = """val x = _x
println("ledger: consumer $label x=" + x)
"""
private const val CONSUMER_AFTER_DISCONNECT =
"""println("ledger: consumer after-disconnect stale-line=" + ((thisBox.getPropertyRaw("preamble") as String?) ?: "").contains("Box._x"))
"""
private const val GRANDCHILD_AFTER_DISCONNECT =
"""println("ledger: grandchild after-disconnect inherits=" + ((thisBox.getPropertyRaw("preamble") as String?) ?: "").contains("Box._x"))
"""
private fun execute(box: String, code: String) = """
- execute:
box: $box
property: code
type: fragment
code: |
${code.trimEnd().prependIndent(" ")}"""
private fun buildTranscript(): String = """
name: context_resolution_ledger
description: Ledger - @exported self-pinning and persisted inherited preamble lines, through the real engine.
operations:
- create:
box: res_exporter
name: Exporter
properties:
code: |
${EXPORTER_V1.trimEnd().prependIndent(" ")}
- create:
box: res_consumer
name: Consumer
properties:
code: |
${consumer("pass1").trimEnd().prependIndent(" ")}
- create:
box: res_grandchild
name: Grandchild
properties:
code: |
${GRANDCHILD_AFTER_DISCONNECT.trimEnd().prependIndent(" ")}
- connect:
parent: res_exporter
child: res_consumer
- connect:
parent: res_consumer
child: res_grandchild""" +
execute("res_exporter", EXPORTER_V1) +
execute("res_consumer", consumer("pass1")) +
execute("res_exporter", EXPORTER_V2) +
execute("res_consumer", consumer("pass2")) +
execute("res_exporter", EXPORTER_READS_OWN) + """
- disconnect:
parent: res_exporter
child: res_consumer""" +
execute("res_consumer", CONSUMER_AFTER_DISCONNECT) +
execute("res_grandchild", GRANDCHILD_AFTER_DISCONNECT) + """
- wait:
millis: 250
"""
fun main(args: Array<String>)
body — 84 lines, from line 136
@JvmStatic
fun main(args: Array<String>) {
startRunloopDaemon()
val document = freshSession("resolution-ledger")
val macrosDir = File(document, "macros").also { it.mkdirs() }
File(macrosDir, "context_resolution_ledger.yaml").writeText(buildTranscript().trimStart())
val failures = mutableListOf<String>()
try {
val run = OperationTranscript.runMacro(document.absolutePath, "context_resolution_ledger")
println("run.success=${run.success} applied=${run.applied} output lines=${run.output.size}")
run.output.forEach { println(" [${it.kind}] ${it.uid}: ${stripHtml(it.html)}") }
if (!run.success) {
failures.add("transcript run failed: ${run.errors.joinToString("; ").ifBlank { run.message }}")
}
val errorLines = run.output.filter { it.kind == "error" || it.kind == "err" }
if (errorLines.isNotEmpty()) {
failures.add("box execution emitted ${errorLines.size} error diagnostic(s): " +
errorLines.joinToString(" | ") { "${it.uid}:${it.line} ${stripHtml(it.html)}" })
}
val printed = run.output.map { stripHtml(it.html) }.filter { it.startsWith("ledger: ") }
if (printed.isEmpty()) {
failures.add("no box printed a ledger line - the transcript did not execute them, so the " +
"comparison below would be between two empty sets")
}
(LEDGER - printed.toSet()).forEach { failures.add("ledger line never printed: \"$it\" (printed: $printed)") }
(printed.toSet() - LEDGER.toSet()).forEach {
failures.add("printed a line the ledger does not account for: \"$it\" - either resolution " +
"changed or the ledger is stale")
}
} catch (t: Throwable) {
failures.add("harness threw ${t.javaClass.simpleName}: ${t.message}")
t.printStackTrace(System.err)
} finally {
thisSessionInfo.remove()
}
if (failures.isNotEmpty()) {
System.err.println("ContextResolutionLedgerSmokeTest FAILED with ${failures.size} problem(s):")
failures.forEach { System.err.println(" - $it") }
exitProcess(1)
}
println("ContextResolutionLedgerSmokeTest ok")
}
private fun freshSession(name: String): File {
thisSessionInfo.remove()
val workspace = Files.createTempDirectory("afield-ctx-$name").toFile()
workspace.deleteOnExit()
val document = File(workspace, "doc").also { it.mkdirs() }
File(document, SavingContext.METADATA_EDN).writeText("{:version 2 :boxes []}\n")
val root = Box("root")
val session = SessionInformation(
filename = document.absolutePath,
documentName = document.name,
boxes = ConcurrentHashMap(),
root = root,
ephemeral = true,
)
root.inside = session
session.boxes[root.id] = root
thisSessionInfo.set(session)
return document
}
private fun startRunloopDaemon() {
val t = Thread({ afield.runtime.lifecycle.Runloop.runLoop.start() }, "context-resolution-ledger-runloop")
t.isDaemon = true
t.start()
val deadline = System.currentTimeMillis() + 30_000
while (afield.runtime.lifecycle.Runloop.runLoop.frame < 1L) {
check(System.currentTimeMillis() < deadline) { "runloop did not start within 30s" }
Thread.sleep(20)
}
println("ContextResolutionLedgerSmokeTest: runloop up (frame=${afield.runtime.lifecycle.Runloop.runLoop.frame})")
}
private fun stripHtml(html: String): String =
html.replace(Regex("<[^>]+>"), " ").replace("<", "<").replace(">", ">").trim()
}