Zum Inhalt

JDL — Testing

Status: Entwurf, Arbeitsstand Version: 0.3.1 Datum: 2026-09-24 Ersetzt: 0.3.0 vom 2026-09-24 Geltungsbereich: wie Tests in Jade beschrieben, gefunden, ausgeführt und isoliert werden; Testumgebungen, Simulation, Topologie, Tests der Meta-Phase und die Tests, die den Kern selbst absichern Nicht hier: Zusicherungen, Fakes, Generatoren und die Ausgabe des Runners im Detail (Nephrit); Scheduler-Mechanik (jdl-vm-scheduler-spec); Entrypoint-Modell (design-context-entrypoints) Quellen: jdl-blueprint-spec v0.4.0, spec-context-model-refactored (§2.1, §10, §12a), Addendum Context/Meta E.7.1, design-context-entrypoints (D.3.3, D.8, D.13.2, D.14.1), jade-fehlermodell v0.4.0 (F-8, F-21a, F-O6), spec-namespaces-und-module §6.2, jdl-vm-scheduler-spec v0.3.0, 12-design-constitution v2.3 (§8.2, §8.4), jade-compiler-db-spec §21.3, Entscheidungen aus den Arbeitsgesprächen (Stand 2026-09-24)


0. Über dieses Dokument

0.1 Kennzeichnung

Marke Bedeutung
[N] bestehend normativ belegt, Fundstelle genannt
[E] entschieden, aber noch in keinem anderen Quelldokument ausgeschrieben
[A] aus bestehendem Material ableitbar
[V] Vorschlag, ausgearbeitet, aber nicht entschieden
[O] offen

0.2 Was dieses Dokument ersetzt

Die Annotation @test samt @test.ignore und @test.only aus dem Annotationen-Entwurf ist ein Überbleibsel und gilt nicht mehr [E]. Tests werden nicht über Annotationen markiert.

0.3 Syntax

Alle Syntaxbeispiele sind spekulativ und mit // spekulativ markiert. Auf spekulative Formen wird keine Folgerung gestützt.

0.4 Sprache, Kern und Nephrit

Das meiste in diesem Dokument ist Arbeit für Nephrit, die Stdlib. Einige Punkte betreffen aber die Sprache oder den Kern in Zig. Sie sind in §12 gesammelt, damit sie nicht zwischen Nephrit-Themen untergehen.


1. Grundsätze

Tests prüfen das echte Programm. Ein Test nutzt dieselben Mechanismen wie die Anwendung: dieselben Blueprints, dieselben Contexts, dieselben Einstiege. Es gibt keine zweite Startlogik und keine Sonderwelt nur für Tests [A] design-context-entrypoints D.3.3.

Die Row sagt, was zu ersetzen ist. Eine Funktion mit ->{UserDb, Clock} braucht im Test genau diese Services. Der Compiler prüft, dass der Test-Context alles bindet. Einen vergessenen Mock gibt es nicht; er ist ein Fehler beim Übersetzen [A] X-2.

Topologie wird geerbt, nicht nachgebaut. Ein Test leitet vom selben Blueprint ab wie die Produktion und tauscht nur Blätter aus (§3) [V].

Struktur ist deklarativ, das Szenario ist Code. Welche Contexts es gibt, beschreibt ein Blueprint. Was im Test passiert, erzählt gewöhnlicher JDL-Code. Viel muss ausprogrammierbar sein, ohne dass man an die Eingeweide muss [E].

Laufzeit-Glue bedient die Topologie, baut sie aber nicht. Testcode darf Contexts nicht zur Laufzeit von Hand verdrahten. Sonst entsteht eine Builder-API, und der Test prüft eine selbstgebaute Welt statt der echten [V].


2. Test-Suites

2.1 Suite und Umgebung sind getrennt [V]

Eine Test-Suite ist ein Blueprint mit dem Root TestSuite. Sie beschreibt Testfälle und wie sie ausgeführt werden. Sie ist kein Context-Profil. Die Umgebung, in der die Fälle laufen, steht in einem eigenen Feld und ist ein Context-Blueprint.

// spekulativ
type ShopTests : TestSuite & {
    context = ShopSystem & {
        scheduler  = .Simulated(seed = 42)
        simulation = .Auto

        web.services = [ServiceBinding { service = Payment, handler = FakePayment }]
    }

    cases = [
        TestCase { name = "Bestellung wird bezahlt",       run = paysOrder },
        TestCase { name = "Ausfall der Bezahlung erholt sich", run = paymentOutage },
    ]
}
  • TestSuite ist der Root der Suite. Er trägt Testfälle und die Einstellungen des Runners, keine Umgebung.
  • context ist die Testumgebung: die echte Topologie ShopSystem, abgeleitet als gewöhnliches Profil. Sie steht als anonyme Blueprint-Komposition direkt im Feld (§11.3).
  • scheduler ist das Kernfeld des Context-Roots, hier mit der Simulationsstrategie (§5.2).
  • simulation = .Auto ist die Root-Einstellung aus §4.4: Fehlende Bindungen werden mit Simulationen ergänzt.
  • web.services bindet im Kind web ausdrücklich etwas anderes und überschreibt damit die automatische Simulation.
  • cases ist eine Liste von Testfällen mit Namen und einer gewöhnlichen Funktion.

Damit ist die Autorität getrennt:

TestSuite    beschreibt Testfälle und Runner-Einstellungen    →  TestEngine
context      beschreibt die Testumgebung                      →  ContextEngine

Die TestEngine wertet die Suite aus und entscheidet, wie Fälle laufen. Über die Umgebung entscheidet allein die ContextEngine, genau wie in Produktion. Die Constitution führt eine TestEngine bereits unter den Ring-2-Engines [N] 12-design-constitution §8.2.

Die Trennung ist eine neue strukturelle Entscheidung, die aus keiner bestehenden Spec folgt. Deshalb steht sie als Vorschlag.

Die Rollen innerhalb der Umgebung [E]:

scheduler                 wie Tasks und Zeit ausgeführt werden
simulation                wie fehlende Service-Bindungen für die simulierte Welt ergänzt werden
ausdrückliche Bindungen   überschreiben die automatische Simulation

Simulation ist keine Component. Sie verändert die Ausführungswelt als Ganzes, und genau das darf eine Component nicht: Sie sieht nur ihre eigenen Werte und stellt Anträge. Es entsteht also keine Component-Kante, kein Contribution-Provider und keine globale Wirkung durch einen lokalen Beitrag.

Was das Feld context annimmt [A]. Es akzeptiert ausschließlich Blueprint-Kompositionen, deren Root ein Context ist, konzeptionell Root(context) <: Context. Das ist hier bewusst keine normative Typregel. Wie sich das statisch ausdrücken lässt, hängt an der offenen Identitäts- und Profilsemantik der Blueprint-Spec (§13, Punkt 3). Die Testing-Spec entscheidet nicht nebenbei, was ShopSystem ontologisch ist.

Ein Testfall selbst ist gewöhnlicher Code:

// spekulativ
def paysOrder() ->{Shop, Throw[TestFailure]} Unit {
    val result = Shop.checkout(testOrder)
    expect(result.status == .Paid)
}
  • Die Row nennt, was der Fall braucht; der Context der Suite muss es binden.
  • expect(...) scheitert über Throw[TestFailure], nicht über einen Trap.

2.2 Warum diese Form

Eine eigene Deklarationsform wie test "name" { ... } bräuchte ein neues Schlüsselwort auf oberster Ebene, und die Bindungen stünden trotzdem woanders. Eine Namenskonvention wäre Magie über Namen und passt nicht zu „Explizitheit vor Annahme“.

In der Blueprint-Form steht alles an einer Stelle: welche Fälle, welche Umgebung, welche Einstellungen. Weitere Testfähigkeiten sind Components an der Suite (& Snapshots, & Properties), Testumgebungen sind gewöhnliche Context-Blueprints, und die Auswertung liegt in Nephrit statt im Compiler.

2.3 Fehlschlag ist ein gewöhnlicher Fehler [A]

Ein fehlgeschlagener Test ist ein Throw[TestFailure]. Der Runner kann darauf reagieren, weitermachen und berichten. Ein Trap bleibt ein Trap: Er beendet den Testfall und wird als dessen Ausgang beobachtet (§6).

2.4 Parametrisierte Tests sind Werte [A]

„Ein Testfall für jede dieser zwanzig Eingaben“ erzeugt keine Deklarationen. Eine Meta-Funktion rechnet eine Liste von TestCase-Records aus, die sich nur in ihrer Eingabe unterscheiden. Records sind überall in der Meta-Phase verwendbar; dafür braucht es keinen neuen Mechanismus.


3. Topologie

3.1 Erben statt nachbauen [V]

Ein Systemtest leitet von der Produktionstopologie ab:

// spekulativ
type ShopSystemTest : ShopSystem & {
    scheduler       = .Simulated(seed = 42)
    simulation      = .Auto
    web.services    = [ServiceBinding { service = Payment, handler = FakePayment }]
    worker.services = [ServiceBinding { service = Queue,   handler = InMemoryQueue }]
}
  • ShopSystem ist die echte Topologie mit denselben Kindern, Grenzen und Bridges.
  • scheduler und simulation stellen die simulierte Welt am Root ein.
  • web.services und worker.services tauschen gezielt Bindungen in einzelnen Kindern.

Ändert sich die Produktionstopologie, ändert sich der Test mit.

Ein so abgeleiteter Context ist genau das, was im Feld context einer Suite steht (§2.1), als benannter Typ wie hier oder als anonyme Blueprint-Komposition direkt im Feld.

3.2 Vier Schichten

Struktur      Context-Baum, Kinder, Grenzen      bleibt identisch
Verbindungen  Bridges zwischen Contexts          bleiben, nur der Transport wird lokal
Umgebung      Services, Resources                werden gezielt getauscht
Zeit          Scheduler-Reihenfolge, Uhr         werden simuliert

Dass Bridges ihre Bedeutung unabhängig vom Transport haben, ist Voraussetzung dafür [A] design-context-entrypoints D.3.2.

3.3 Kinder haben Namen [E]

Kind-Contexts sind benannte Felder, keine anonyme Liste. Das entspricht der Beziehung „Feld“ im Context-Modell [N] spec-context-model-refactored §10.1.

// spekulativ
type ShopSystem : Context & {
    var web:    WebContext
    var worker: WorkerContext
}

Für eine wechselnde Anzahl gleichartiger Kinder bleibt eine Liste zulässig. Sie wird im Ganzen angesprochen, nicht über ihre Position.

3.4 Überschreiben über einen Pfad [E]

Ein Pfad beim Überschreiben ist eine Abkürzung:

web.services = X        heißt        web = web & { services = X }

Längere Pfade gehen genauso, etwa web.tls.certificate = ... durch das Kind in dessen Component.

Das Überschreiben geschieht in der Beschreibung, bevor eine Engine sie auswertet. Die Engine des Kindes bekommt eine andere Beschreibung und prüft sie wie jede andere. Ihre Hoheit über die Auswertung bleibt erhalten.

Regel. Ein Pfad kann durch Felder und durch Components laufen, und beide werden verschieden behandelt:

  • Jedes durchlaufene Feld muss eine Spezialisierung erlauben, also ein var sein.
  • Eine Component hat kein val oder var. Sie darf durchlaufen werden, aber eine Pfadüberschreibung darf sie weder hinzufügen noch entfernen noch ersetzen. Die Menge der Components bleibt fest.
  • Das Zielfeld muss ein var sein.
web.tls.certificate = newCert

web          Feld        muss var sein
tls          Component   keine var-Frage, darf nur durchlaufen werden
certificate  Feld        muss var sein

Ein Autor, der ein Kind mit val festlegt, verbietet damit, dass jemand von außen darin etwas ändert.

3.5 Ein Kind ganz ersetzen [V]

web = GanzAndererWebContext ist zulässig, wenn web ein var ist. Dann prüft der Test aber eine andere Topologie als die Produktion. Die Test-Engine meldet das als Hinweis: „Dieser Test ersetzt das Kind web vollständig.“


4. Die Testumgebung

4.1 Services tauschen [N] Addendum E.7.1

Dasselbe Muster wie Shadow-Bindungen: Db → ShadowDb, Clock → DeterministicClock, Network → MockNetwork, Storage → ScratchStorage. Der Anwendungscode fragt nirgends, ob er im Test läuft.

4.2 Simulationen an Services [E]

Ein Service kann eine deterministische Implementierung angeben, die in simulierten Welten gebunden wird. Sie hängt als Refinement am Service:

// spekulativ
type Db : service {
    def query(sql: str) ->{Throw[DbError]} RowSet
} :> Simulation(InMemoryDb)
  • Der Service bleibt ein reiner Vertrag.
  • :> Simulation(InMemoryDb) nennt die Implementierung, die in simulierten Welten gebunden wird.

Liefert ein Service Ressourcen, liefert seine Simulation simulierte Ressourcen, samt Scopeable, damit with darauf funktioniert.

4.3 Wer eine Simulation mitbringen muss [E]

fundamentale Services (Nephrit, nahe an Ring 0)   besitzen eine Standardsimulation    Clock, Random, Fs, Net, …
andere Services, Plugins                          dürfen eine deklarieren

Fehlt einem Service die Simulation, muss ein simulierter Test ihn ausdrücklich binden. Die Meta-Phase meldet sonst: „Service X hat keine Simulation. Binde im Test-Context ausdrücklich etwas oder ergänze :> Simulation(...).“

Für viele Benutzer- und Plugin-Services gibt es keine eindeutige kanonische Simulation. Eine Pflicht würde dort nur Fakes erzeugen, die mehr nach Abgabetermin als nach Sorgfalt aussehen.

4.4 Simulationen automatisch binden [V]

Ein Test-Context mit simulation = .Auto bindet für jeden Service in den Rows dessen Simulation, außer der Test bindet ausdrücklich etwas anderes. „Explizitheit vor Annahme“ gilt.

Welche Services ein Programm braucht, weiß nur die ContextEngine, weil sie ohnehin prüft, ob alles gebunden ist. Das Auffüllen ist deshalb eine Einstellung am Root des Contexts, keine Component: Ein Provider sieht nur seine eigenen Werte.

4.5 Simulationen werden selbst geprüft [E]

Ein Fake, das sich anders verhält als das Original, ist schlimmer als keins. Zu jedem Service gibt es deshalb eine gemeinsame Testsammlung, die gegen die echte Implementierung und gegen die Simulation läuft. (Suchwort: contract testing.)

Ein modulweiter Erzeuger kann diese Sammlung für jeden Service mit Simulation automatisch anlegen, mit ableitbarem Namen wie Db.ContractTests [A] spec-namespaces-und-module §6.2.

4.6 Steuerung ist ein Service [E]

Ein Test braucht Eingriffe in die simulierte Welt: eine Bridge kappen, ein Kind beenden, die Uhr vorspulen. Diese Fähigkeiten sind Services in der Row, etwa Faults und SimClock.

// spekulativ
def paymentOutage() ->{Shop, Faults, SimClock, Throw[TestFailure]} Unit {
    with Faults.cut(.webToPayment) as outage {
        val result = Shop.checkout(testOrder)
        SimClock.advance(30.s)
        expect(result.status == .Retrying)
    }
    expect(Shop.checkout(testOrder).status == .Paid)
}
  • Faults und SimClock stehen in der Row wie jeder andere Service.
  • Faults.cut(...) liefert eine Ressource; gescopet wird die Ressource, nicht der Service.
  • Innerhalb des Blocks ist die Bridge gekappt; am Ende stellt der Austrittspunkt von Scopeable sie wieder her.
  • SimClock.advance(30.s) spult die simulierte Uhr vor, ohne dass echte Zeit vergeht.
  • Die letzte Zeile prüft, dass sich das System danach erholt.

Nur ein Test-Context bindet Faults und SimClock. Verwendet Produktionscode sie, bindet der Produktions-Context sie nicht, und das ist ein Fehler beim Übersetzen.

[O] Die Constitution braucht eine Regel, die einem Produktions-Context verbietet, Steuerungs-Services überhaupt zu binden.

4.7 Aufräumen mit with und die Trap-Regel [E]

Eingriffe und Wegwerfressourcen werden über with gescopet. Das Aufräumen kann man nicht vergessen, und ein Eingriff hat im Code eine sichtbare Dauer.

Der Austrittspunkt von Scopeable ist JDL-Code. Nach einem Trap läuft kein JDL-Code mehr [N] jade-fehlermodell F-8. Deshalb gilt:

Regel. Jede Ressource, mit der ein Test in die Welt eingreift, gehört zur simulierten Welt dieses Tests. Sie darf ihren Test nie überleben.

normaler Ablauf und Throw    with räumt auf          JDL-Code
Trap                         der Context stirbt      mechanisch, Ring 0

Trappt ein Test, stirbt seine ganze Welt, und alles wird mit ihr mechanisch freigegeben. Es bleibt nichts zurück, das wiederhergestellt werden müsste.

4.8 Determinismus in Teilen eines Tests [V]

Die Scheduler-Strategie ist Kern eines Contexts und lässt sich nicht mitten im Lauf umstellen. Ein with über eine zeitlich begrenzte Ableitung des Contexts erzeugt dagegen ein Kind mit eigener Strategie, das genau so lange lebt wie der Block. „Hier deterministisch mit Startwert 7, danach wieder normal“ wird so zu einem kurzlebigen Kind, nicht zu einem Umschalten im laufenden Scheduler.


5. Simulation

5.1 Wozu [E]

Nebenläufige Fehler hängen oft an einer seltenen Reihenfolge. Im normalen Betrieb bestimmen Kleinigkeiten wie die Last des Rechners, welche Task wann läuft. Ein Fehler tritt dann einmal auf und beim zweiten Versuch nicht mehr.

In der Simulation übernimmt der Scheduler die Rolle des Zufalls, gesteuert durch einen festen Startwert. Gleicher Startwert, gleiche Reihenfolge, gleiches Ergebnis. (Suchwort: deterministic simulation testing.)

5.2 Die Betriebsart des Schedulers [E]

Der Simulationsmodus ist eine weitere Scheduler-Strategie im Kernfeld des Context-Roots, etwa .Simulated(seed = 42). Nur der Root setzt sie.

normal                           simuliert
mehrere Worker, Work-Stealing    ein Worker, Auswahl bereiter Tasks mit Startwert
echte Uhr                        gespielte Uhr, die zur nächsten fälligen Zeit springt
Event-Poller für echtes I/O      aus; Ereignisse kommen von simulierten Services

Tasks, Warten, Wecken und Abbrechen bleiben unverändert. Getestet wird dieselbe Maschine mit einem anderen Taktgeber.

Kanonische Ordnung [E]. Ein gleicher Startwert reicht nicht, wenn die Kandidaten, aus denen gewählt wird, selbst in zufälliger Reihenfolge vorliegen. Deshalb gilt im Simulationsmodus:

  • Jede neue Task bekommt eine stabile, fortlaufend vergebene Simulationsidentität.
  • Gleichzeitig bereite Tasks werden kanonisch geordnet.
  • Gleichzeitig fällige Timer werden kanonisch geordnet.
  • Gleichzeitig eintreffende Ereignisse simulierter Services werden kanonisch geordnet.
  • Speicheradressen, Kennungen von Host-Threads, die Reihenfolge von Hash-Durchläufen und andere prozessabhängige Werte beeinflussen diese Ordnung nie.

Die konkrete Warteschlangen- oder Sortierimplementierung ist ein Detail in Zig.

Seeds, die simulierte Services intern benutzen, müssen ebenfalls reproduzierbar und unabhängig von Zufällen des Hosts oder der Initialisierung sein. Wie sie aus dem Startwert abgeleitet werden, ist Implementierungsdetail.

5.3 Was die Simulation abdeckt, und was nicht [A]

Die Simulation erzeugt zulässige Ausführungsreihenfolgen des Schedulers. Die Scheduler-Spec verlangt, dass das Ergebnis einer Jade-Task nicht davon abhängt, auf welchem Worker sie läuft [N] jdl-vm-scheduler-spec S-21. Der Simulationsmodus wählt deshalb nur Reihenfolgen, die auch im echten Betrieb möglich sind. Ein Fehler, den er in der Zusammenarbeit von Tasks findet, ist ein echter Fehler.

Nicht abgedeckt ist echte Gleichzeitigkeit im Speicher: Atomics, lock-freie Mechanik, Ring-0-Code und native Parallelität. Ein einzelner Worker kann solche Überlappungen nicht erzeugen. Die Simulation ist kein Ersatz für Tests echter paralleler Speicherzugriffe. Gerade der Zig-Kern braucht dafür eigene Werkzeuge.

Ein fehlgeschlagener Test meldet seinen Startwert. Mit diesem Startwert lässt er sich beliebig oft exakt wiederholen.

5.4 Steuerung von außen [E]

advance ist nicht reentrant [E]. Die aufrufende Test-Task treibt den Scheduler nicht selbst an. Sie wartet wie jede andere Task:

Test-Task ruft SimClock.advance(d)
    ↓
Test-Task wartet auf Advance(Ziel = jetzt + d)
    ↓
der normale Scheduler-Lauf geht weiter und arbeitet bereite Tasks ab
    ↓
erst wenn keine Task mehr bereit ist, springt die gespielte Zeit
zum nächsten fälligen Zeitpunkt, höchstens bis zum Ziel
    ↓
Ziel erreicht, alle bis dahin fälligen Ereignisse verarbeitet
    ↓
Test-Task wird geweckt

Die Zeit springt nur bei Ruhe [E]. Die gespielte Zeit rückt nur vor, wenn der Scheduler keine bereite Arbeit mehr hat. Solange Tasks bereit sind, bleibt die Zeit stehen.

Budget gegen stillstehende Zeit [E]. Tasks, die sich endlos gegenseitig erzeugen, würden die Zeit nie vorrücken lassen. Deshalb besitzt die TestEngine, nicht der Scheduler, ein Budget für Scheduling-Entscheidungen ohne Fortschritt der gespielten Zeit. Der Scheduler liefert nur die Mechanik und macht die Entscheidungen zählbar. Die TestEngine entscheidet, dass das Budget überschritten ist.

Eine Überschreitung ist ein gewöhnlicher Testfehler, kein Trap:

SimulationStalled
    seed              Startwert der Welt
    virtualTime       gespielte Zeit beim Abbruch
    schedulingSteps   Entscheidungen ohne Zeitfortschritt

Mit dem Startwert ist auch dieser Fehler exakt wiederholbar.

[V] Ein step(), das genau eine Scheduling-Entscheidung ausführt, kann später auf derselben Mechanik aufgebaut werden.

5.5 Nur simulierte Services [E]

In einer simulierten Welt sind nur simulierte Services zulässig. Ein echter nativer Aufruf, der auf einer Lane oder im Blocking-Pool laufen würde, ist ein Fehler beim Übersetzen; der Context weiß, was er bindet. Fundamentale Services bringen ihre Simulation mit (§4.3); für alle anderen bindet der Test ausdrücklich etwas.

5.6 Zwei Bedeutungen von „deterministisch“ [V]

Wer eine Ressource berührt, trägt das Label Deterministic nicht. Eine Testfunktion, die Faults.cut benutzt, ist also nicht Deterministic, obwohl sie in einer reproduzierbaren Welt läuft.

Deterministic (Label)      Eigenschaft einer Funktion, statisch geprüft
reproduzierbare Welt       Eigenschaft einer Testumgebung, zur Laufzeit

Um Anfänger nicht zu verwirren, heißt die zweite in Nephrit und in der Dokumentation nicht „deterministisch“, sondern „Simulation mit Startwert“ oder „reproduzierbare Welt“.

5.7 Kurzform in Nephrit [V]

Wer die zwei Root-Felder nicht jedes Mal ausschreiben will, bekommt in Nephrit eine Hilfsfunktion:

// spekulativ
Simulated(ShopSystem, seed = 42)

Sie ist eine Meta-Funktion, die das Profil baut, und ist gleichwertig zu:

// spekulativ
ShopSystem & {
    scheduler  = .Simulated(seed = 42)
    simulation = .Auto
}

Das ist Bequemlichkeit in der Bibliothek, keine neue Sprachsemantik. Es entsteht keine Component-Kante. Der Name der Hilfsfunktion ist vorläufig.


6. Ausführung und Isolation

6.1 Ein Context pro Testfall [V]

Die Test-Engine führt jeden Testfall in einem eigenen Kind-Context unter dem Runner aus, gebildet aus dem Feld context der Suite. Isolation entsteht nur durch einen weiteren Context [N] jade-fehlermodell F-21a.

Eine frische Laufzeitinstanz pro Testfall [E]. Jeder Testfall materialisiert eine eigene Laufzeitinstanz seiner Testumgebung, nicht nur einen neuen Descriptor. Zustandsbehaftete Bindungen, Ressourcen und Handles der Testumgebung werden zwischen Testfällen nicht stillschweigend geteilt. Gemeinsamer äußerer Zustand muss ausdrücklich als solcher gebunden sein. Wie Arenen, Pools und Speicher dafür bereitgestellt werden, ist Implementierungsdetail.

Trappt ein Testfall, stirbt nur seine Domäne. Der Runner sieht den Ausgang Trap und macht weiter. So lassen sich auch Tests schreiben, die einen Trap erwarten, ohne dass Traps in der Sprache fangbar werden. Erwartete Traps sind Semantik des Runners in Nephrit, keine neue Mechanik in Ring 0.

6.2 Genau so viel Umgebung wie nötig [A]

Weil die Test-Engine die tatsächliche Row jedes Testfalls kennt, bindet sie jedem Fall genau, was er braucht. Ein Fall mit ->{} läuft in einer wirklich leeren Umgebung.

6.3 Kosten [O]

Ein Context pro Testfall setzt voraus, dass Kind-Contexts billig genug sind. Das Fehlermodell führt genau diese Kosten als offenen Punkt [N] jade-fehlermodell F-O6.


7. Tests der Meta-Phase

7.1 Descriptor-Schnappschüsse [A]

Ein Blueprint ergibt einen Descriptor. Der Test vergleicht ihn mit einem gespeicherten Stand. Weil Descriptoren kanonisch serialisiert werden, sind solche Vergleiche stabil [N] jade-compiler-db-spec §21.3. Geeignet für Provider und Engines.

7.2 Trockenlauf [A]

describe ohne instantiate ist ein Trockenlauf [N] jdl-blueprint-spec §9.1. Ein Engine-Test prüft, was die Engine vorhat, ohne dass sie etwas tut.

7.3 Diagnose-Tests [V]

Tests, die erwarten, dass etwas nicht übersetzt. Sie laufen über eine eigene Compiler-Instanz, die der Test als Service bekommt. Gerade Regeln wie „zwei Components für dieselbe exklusive Stelle sind ein Konflikt“ lassen sich nur so absichern.

Verglichen wird nicht der Meldungstext, sondern:

DiagnosticId          feste Kennung der Diagnoseart
strukturierte Angaben die Werte, die die Diagnose trägt
SourceSpan            die betroffene Stelle im Quelltext

Der Meldungstext kann zusätzlich geprüft werden, muss aber nicht. Sonst würde jede verständlichere Formulierung einer Fehlermeldung Tests brechen, und verständliche Meldungen sind für Anfänger wichtig.

Das setzt voraus, dass Diagnosen eine stabile Kennung und strukturierte Angaben tragen. Das ist eine neue Anforderung an das Diagnosemodell der CompilerDB.

7.4 Topologie und Sicherheit als Abfrage [V]

Struktur und Autorität stehen materialisiert in den Descriptoren. Viele Topologie-Tests sind deshalb Abfragen über einen Graphen und laufen in der Meta-Phase:

  • Erreicht der Web-Context die Datenbank nur über den Service UserDb?
  • Hat irgendein Kind mehr Autorität als sein Elternteil?
  • Gibt es eine Bridge vom Plugin-Context nach draußen?

Solche Tests schlagen fehl, bevor das Programm existiert.


8. Finden und Auswählen

8.1 Tests über ihren Typ finden [V]

Die CompilerDB findet Tests über eine gewöhnliche Abfrage: alle Typen mit TestSuite als Root. Keine Annotation, kein eigener Durchlauf.

8.2 Nur betroffene Tests [V]

Die CompilerDB kennt die Abhängigkeiten der Meta-Phase zwischen Descriptoren. Daraus lässt sich eine Auswahl der Tests ableiten, die eine Änderung betreffen könnte. (Suchwort: test impact analysis.)

Diese Abhängigkeiten beweisen aber keine vollständige Kette aller Aufrufe zur Laufzeit. Dispatch, generische Instanzierungen und dynamische Aufrufe über Dyn sind darin nicht zwingend enthalten. Die Auswahl muss deshalb konservativ sein: lieber zu viele Tests ausführen als einen relevanten übersehen. Wann die Abhängigkeiten dafür ausreichen, ist offen.

8.3 Tests im Produktionsbuild [A]

Blueprints leben nur in der Meta-Phase. Eine Test-Suite, auf die kein Produktionseinstieg verweist, wird zur Laufzeit nie ausgewertet. Nur der Test-Runner als eigener Einstieg bezieht sie ein. @cfg-Schalter für Tests sind dafür nicht nötig.

Ob sämtliche Bytes von Testcode aus Produktionsartefakten verschwinden, ist eine Frage von Verpackung und Linker, kein Blocker für das Testmodell.


9. Einstiege

9.1 Der Runner ist ein Einstieg [V]

Der Test-Runner ist ein Einstieg. Einzelne Tests sind keine Einstiege, sondern Fälle, die er in Kind-Contexts ausführt. „Alles ist Entrypoint“ ist ausdrücklich als Risiko genannt [N] design-context-entrypoints D.14.1.

9.2 Anwendungseinstiege testen [A]

Der Runner schickt eine künstliche InitialInvocation über einen Mock-Transport an denselben Einstieg, den auch die Shell benutzen würde [N] design-context-entrypoints D.3.3, D.13.2. Der Test prüft den Weg, den das echte Programm geht.

9.3 Mehrere Einstiege [A]

Mit mehreren Einstiegen hat jede Rolle ihren eigenen Root-Context und ihre eigene Autorität. Man testet „die Web-Rolle“ und „die Worker-Rolle“, jede in ihrem Rahmen. Ein Systemtest startet mehrere Einstiege als eingebettete Domänen in einer gemeinsamen simulierten Welt [N] design-context-entrypoints D.8.

9.4 Mehrere Knoten [A]

Mit Jade Node lassen sich mehrere Knoten in einem Prozess simulieren, verbunden über lokale Transporte, in simulierter Reihenfolge. Das ist keine Sondertechnik, sondern dieselbe Topologie mit anderen Blättern.


10. Tests, die den Kern absichern

Diese Tests müssen in Zig existieren, bevor die erste JDL-Engine läuft.

10.1 Differenztest der CompilerDB [E]

Einmal alles vollständig neu berechnen, einmal inkrementell. Beide Ergebnisse müssen identisch sein. Dieser Test ist wertvoller als jede interne Architekturentscheidung.

10.2 Determinismustest der Meta-Phase [A]

Zweimal bauen, gleiche Descriptor-Hashes. Bricht das, bricht der Cache, und niemand merkt es sofort.

10.3 Simulation gegen normalen Betrieb [V]

Derselbe Test in der Simulation und im normalen Betrieb muss dasselbe Ergebnis liefern, sofern er nicht von Reihenfolgen abhängt. Das prüft, dass der Simulationsmodus wirklich nur den Taktgeber tauscht.


11. Sprachvoraussetzungen

11.1 Begrenzte und offene Funktionsfelder

Ein Funktionsfeld in einem Blueprint gibt es in zwei Arten:

begrenzt   "höchstens diese Row"                         Testfälle, Handler in bekannter Umgebung
offen      "irgendeine Row; der Context muss sie binden"  Haken, Middleware, Slots aus Components

Begrenzt [E]: Eine Funktion, die weniger verlangt, passt überall dorthin, wo mehr erlaubt ist. Für Throw gilt dasselbe. Wer die Funktion aufruft, muss alles bereithalten, was im Feldtyp steht.

Offen [E] für den Grundsatz, [O] für die Typregel: Die Prüfung verschiebt sich von „passt die Funktion zum Feldtyp?“ zu „bindet der Context, was die eingesetzten Funktionen brauchen?“.

Das ist nur unter drei Bedingungen beherrschbar:

  • Offene Funktionsfelder gibt es nur in Blueprints.
  • Die konkrete Funktion und ihre konkrete Row sind spätestens während der Meta-Auswertung bekannt.
  • Kein Struct und kein Laufzeitwert darf eine Funktion mit offener Row aufbewahren. Die unbekannte Row entkommt der Meta-Phase nie.

Unter diesen Bedingungen ist ein fehlender Service auch bei offenen Feldern ein Fehler beim Übersetzen.

Allgemein betrachtet ist ein offenes Feld eine Row, die erst später geöffnet wird; das ist schwere Typtheorie. Die Bedingungen oben halten sie aus Jade heraus, ersetzen aber keine formale Regel.

[O] Offen ist die genaue Typregel: Was ist der tatsächliche Typ eines offenen Feldes? Wann wird die konkrete Row geöffnet? Wie verhalten sich Listen mit Funktionen unterschiedlicher Rows, etwa cases? Die Schreibweise ist die kleinste dieser Fragen.

11.2 Benannte Kinder und Pfadüberschreiben [E]

Siehe §3.3 und §3.4.

11.3 Anonyme Blueprint-Komposition [E]

A & { ... } darf in einem Feld stehen, ohne eigene Typdeklaration. Das braucht man für Listen von Kind-Contexts, für generierte Topologien, für Oberflächen aus Bausteinen und für das Feld context einer Suite.

Eine anonyme Blueprint-Komposition ist ein Stück Meta-Beschreibung, kein Laufzeitwert. Sie lebt wie jeder Blueprint nur in der Meta-Phase.

Drei Regeln:

  • Keine eigene Identität. Sie lässt sich nicht von anderswo beim Namen nennen. Sie existiert dort, wo sie steht.
  • Die Form steht dort fest, wo sie geschrieben ist. Ein Ort, an dem man sie ablesen kann.
  • Die CompilerDB erkennt Gleiches wieder. Zwei gleiche anonyme Kompositionen bekommen dieselbe kanonische Darstellung.

11.4 Was Meta-Funktionen erzeugen dürfen [A]

Werte ausrechnen              gewöhnliche Meta-Funktion        parametrisierte Tests
neue benannte Typen           modulweiter Erzeuger (§6.2)      Db.ContractTests
Blueprints ohne eigenen Namen anonyme Blueprint-Komposition (§11.3)  fünf Worker

Neue benannte Typen regelt bereits spec-namespaces-und-module §6.2: nur im eigenen Baum, gegen einen Schnappschuss, in FQN-Reihenfolge, mit ableitbarem Namen.


12. Was Sprache und Kern betrifft

Punkt Ort Abschnitt
Anonyme Blueprint-Komposition Blueprint-Spec §11.3
Root- und Identitätsregel für Blueprint-Kompositionen, damit TestSuite.context statisch auf Context-Roots beschränkt werden kann Blueprint-Spec, Typsystem §2.1
Offene Funktionsfelder nur in Blueprints, formale Row-Regel Blueprint-Spec, Typsystem §11.1
Benannte Kinder, Pfadüberschreiben Blueprint-Spec §3.3, §3.4
Simulationsmodus: schedulerseitige Reihenfolgen, Startwert, gespielte Uhr, kanonische Ordnung, nicht-reentrantes advance mit Zeitsprung nur bei Ruhe, zählbare Entscheidungen; keine Aussage über echte Speicherparallelität Scheduler-Spec, Ring 0 §5
Frische Laufzeitinstanz pro Testfall Runtime, Ring 0 §6.1
Mechanik für Eingriffe: Domäne beenden, Bridge kappen oder verzögern Ring 0 §4.6
Konservative Auswahl betroffener Tests CompilerDB §8.2
Stabile DiagnosticId, strukturierte Angaben, SourceSpan CompilerDB, Diagnosemodell §7.3
Kanonische Darstellung anonymer Kompositionen CompilerDB §11.3
Verbot, Steuerungs-Services in Produktion zu binden Constitution §4.6
Differenz- und Determinismustest Zig §10
Eingebettete Einstiege Entrypoint-Modell §9.3

Alles andere ist Nephrit: TestSuite, Test-Engine, Fakes, Standardsimulationen der fundamentalen Services, Faults- und SimClock-Handler auf Basis der Ring-0-Mechanik, Zusicherungen, Generatoren, die Ausgabe des Runners.


13. Offene Punkte

# Punkt Abschnitt
1 Trennung von Suite und Umgebung (Vorschlag) §2.1
2 Formale Typregel für offene Funktionsfelder §11.1
3 Typ des Feldes context: Wie wird statisch ausgedrückt, dass eine anonyme Blueprint-Komposition einen Context-Root besitzt? Abhängig von der offenen Identitäts- und Profilsemantik der Blueprint-Spec §2.1
4 Sichtbarkeit: Dürfen Tests an Interna ihres Moduls? Regel in der Constitution —
5 Constitution-Regel gegen Steuerungs-Services in Produktion §4.6
6 Wann die Abhängigkeiten für eine Auswahl betroffener Tests ausreichen §8.2
7 Stabile Kennungen und strukturierte Angaben im Diagnosemodell §7.3
8 Kosten eines Contexts pro Testfall (jade-fehlermodell F-O6) §6.3
9 step() im Simulationsmodus §5.4
10 Name der Nephrit-Hilfsfunktion Simulated §5.7
11 Form der Compiler-Instanz für Diagnose-Tests §7.3

13.1 Bewusst nicht ausgearbeitet

Die folgenden Punkte stehen nur als Anforderung. Sie werden hier nicht weiter ausgebaut; keiner davon blockiert einen ersten Simulator.

Punkt Anforderung
simulation = .Auto automatische simulierte Bindung durch die ContextEngine; die genaue transitive Hülle der Bindungen wird später formalisiert
Seeds simulierter Services reproduzierbar und unabhängig von Zufällen des Hosts oder der Initialisierung; die Ableitung ist Implementierungsdetail
Mehrere Einstiege, mehrere Knoten später eine gemeinsame simulierte Welt mit gemeinsamer gespielter Zeit und gemeinsamem Startwert
Erwartete Traps Semantik des Runners in Nephrit, keine neue Mechanik in Ring 0
Auswahl betroffener Tests bleibt Optimierung und konservativ
Form der Diagnose-Test-API Arbeit für CompilerDB und Nephrit
step() später auf derselben Scheduler-Mechanik
Testcode in Produktionsartefakten Verpackung und Linker

Nicht jetzt ausgearbeitet werden außerdem: Oberflächen des Runners, Komfort-APIs in Nephrit, Optimierungen der Testauswahl, die Detailform der Diagnose-API und eine vollständige Teststeuerung über mehrere Knoten.


14. Regeln

T-1   Tests werden nicht über Annotationen markiert; @test gilt nicht mehr.        [E]
T-2   Ein Test nutzt dieselben Blueprints, Contexts und Einstiege wie die
      Anwendung; es gibt keine eigene Startlogik für Tests.                          [A]
T-3   Kind-Contexts sind benannte Felder.                                            [E]
T-4   Ein Pfad beim Überschreiben ist ein & auf jede Station. Durchlaufene
      Felder und das Zielfeld müssen var sein. Components haben kein val/var;
      sie dürfen durchlaufen, aber per Pfad weder hinzugefügt, entfernt noch
      ersetzt werden.                                                                [E]
T-5   Fundamentale Services besitzen eine Standardsimulation; andere Services
      dürfen eine deklarieren; sonst bindet ein simulierter Test ausdrücklich.       [E]
T-6   Simulationen werden gegen ihr Original geprüft.                                [E]
T-7   Steuerung einer simulierten Welt geschieht über Services in der Row.           [E]
T-8   Eine Ressource, mit der ein Test eingreift, überlebt ihren Test nie.           [E]
T-9   Die Scheduler-Simulation verändert nur Zeit- und Scheduling-Mechanik;
      die Bedeutung von Tasks bleibt unverändert. Sie erzeugt nur zulässige
      Reihenfolgen des Schedulers und sagt nichts über echte
      Speicherparallelität aus.                                                      [A]
T-10  In einer simulierten Welt sind nur simulierte Services zulässig.               [E]
T-11  Ein Funktionsfeld ist begrenzt oder offen. Offene Felder gibt es nur in
      Blueprints; keine Laufzeitstruktur bewahrt eine offene Row auf.               [E]
T-12  Eine anonyme Blueprint-Komposition ist Meta-Beschreibung, hat keine
      eigene Identität und wird über ihren Inhalt wiedererkannt.                     [E]
T-13  Der Differenztest der CompilerDB und der Determinismustest der
      Meta-Phase existieren in Zig vor der ersten JDL-Engine.                        [E]/[A]
T-14  Eine Test-Suite ist kein Context; sie trägt ihre Umgebung in einem
      Context-Feld, über das die ContextEngine entscheidet.                          [V]
T-15  Diagnose-Tests vergleichen Kennung, strukturierte Angaben und Stelle;
      der Meldungstext ist optional.                                                 [V]
T-16  Die Auswahl betroffener Tests ist konservativ.                                 [V]
T-17  Eine simulierte Testumgebung darf zusätzlich Services und externe
      Ereignisquellen durch simulierte Entsprechungen ersetzen.                      [E]
T-18  Simulation ist Root-Konfiguration der Testumgebung (scheduler,
      simulation), keine Component.                                                  [E]
T-19  Im Simulationsmodus bekommt jede Task eine stabile, fortlaufende
      Simulationsidentität; bereite Tasks, fällige Timer und Ereignisse
      simulierter Services werden kanonisch geordnet; prozessabhängige Werte
      beeinflussen die Ordnung nie.                                                  [E]
T-20  SimClock.advance ist nicht reentrant: Die aufrufende Task wartet, und
      die gespielte Zeit rückt nur vor, wenn keine Task bereit ist.                  [E]
T-21  Das Budget für Entscheidungen ohne Zeitfortschritt gehört der
      TestEngine; seine Überschreitung ist der Testfehler SimulationStalled,
      kein Trap.                                                                     [E]
T-22  Jeder Testfall materialisiert eine eigene Laufzeitinstanz seiner
      Testumgebung; Zustand wird zwischen Testfällen nicht stillschweigend
      geteilt.                                                                       [E]

15. Auswirkungen auf andere Dokumente

Dokument Was nachzuziehen ist
jdl-blueprint-spec anonyme Blueprint-Komposition; offene Funktionsfelder ausschließlich in Blueprints; formale Row-Regel; benannte Kinder und Pfadüberschreiben; Root- und Identitätsregel für Kompositionen (für TestSuite.context)
jdl-vm-scheduler-spec Strategie .Simulated mit Startwert, gespielter Uhr, Poller aus; kanonische Ordnung mit stabiler Simulationsidentität; nicht-reentrantes advance, Zeitsprung nur bei Ruhe, zählbare Entscheidungen; Simulation erzeugt schedulerseitige Reihenfolgen, keine Aussage über echte Speicherparallelität
jade-compiler-db-spec konservative Auswahl betroffener Tests; stabile DiagnosticId, strukturierte Angaben, SourceSpan; kanonische Darstellung anonymer Kompositionen
spec-context-model-refactored Kinder als benannte Felder; Root-Feld simulation (Auffüllen mit Simulationen) neben scheduler
spec-constitution-system Regel gegen Steuerungs-Services in Produktion; Sichtbarkeit für Tests
JDL-Compiler-Annotationen-konsolidiert @test streichen

Änderungsprotokoll

0.3.1 (2026-09-24) — Präzisionsfassung, keine neue Architektur. Pfadüberschreiben unterscheidet Felder und Components (§3.4, T-4). Kanonische Ordnung im Simulationsmodus mit stabiler Simulationsidentität (§5.2, T-19). advance nicht reentrant, gespielte Zeit rückt nur bei Ruhe vor; Budget der TestEngine mit Testfehler SimulationStalled (§5.4, T-20, T-21). Frische Laufzeitinstanz pro Testfall (§6.1, T-22). Erwartete Traps als Runner-Semantik, Testcode in Produktionsartefakten als Verpackungsfrage (§6.1, §8.3). Neu §13.1: bewusst nicht ausgearbeitete Punkte.

0.3.0 (2026-09-24) — Autorität der Simulation korrigiert: Simulated ist keine Component mehr, sondern Root-Konfiguration der Testumgebung über scheduler = .Simulated(seed) und simulation = .Auto (§2.1, §3.1, T-18). Die frühere Verwendung als Component widersprach §4.4. Kurzform Simulated(...) als Meta-Hilfsfunktion in Nephrit, ohne neue Sprachsemantik (§5.7). T-9 geschärft, T-17 neu: Scheduler-Simulation ändert nur Zeit- und Scheduling-Mechanik; die Testumgebung darf zusätzlich Services und Ereignisquellen ersetzen. Typ des Feldes context als offener Punkt, abhängig von der Identitäts- und Profilsemantik der Blueprint-Spec, ausdrücklich nicht hier entschieden.

0.2.0 (2026-09-24) — Verantwortungsgrenze geändert: TestSuite ist kein Context-Profil mehr, sondern trägt ihre Umgebung in einem Feld context; Autorität getrennt zwischen TestEngine und ContextEngine (§2.1, [V]). „Namenlose Komposition als Wert“ heißt jetzt „anonyme Blueprint-Komposition“ und ist ausdrücklich Meta-Beschreibung, kein Laufzeitwert (§11.3). Offene Funktionsfelder nur in Blueprints, ohne Aufbewahrung in Laufzeitstrukturen; die Typregel ist offen (§11.1). Simulation auf schedulerseitige Reihenfolgen begrenzt, keine Aussage über echte Speicherparallelität (§5.3). Auswahl betroffener Tests konservativ und [V] (§8.2). Diagnose-Tests vergleichen Kennung, strukturierte Angaben und Stelle (§7.3). Simulationen: Standard für fundamentale Services, sonst optional mit ausdrücklicher Bindung (§4.2, §4.3). §12, §13, §14 und §15 angepasst; neue Regeln T-14 bis T-16.

0.1.0 (2026-09-24) — Erster Entwurf. Tests als Blueprints, Topologie erben statt nachbauen, Simulation pro Service mit abgestufter Pflicht und Vertragstests, Steuerung als Services, Aufräumen über with mit Trap-Regel, Simulationsmodus des Schedulers, Tests der Meta-Phase, Finden über die CompilerDB, Einstiege, Kern-Tests. Sprachvoraussetzungen: begrenzte und offene Funktionsfelder, benannte Kinder, Pfadüberschreiben, namenlose Komposition.