Files
lmcp/tests/phase_b_acceptance.lua
T
Markus Fritsche e44b35b0e0 tests: Abnahmetest fuer Phase B — Fassungen durchsetzen
Phase A stellte fest, welche Fassungen verlangt werden. Phase B lehnt
ab, was der Server nicht spricht. Gemessen am 2026-08-08 an
hertz-tools:8080 antwortet lmcp mit HTTP 200 auf JEDE Fassungsangabe,
auch auf 1999-01-01 - eine Fassung, die es nie gab. Es liest den Kopf
nicht; `Mcp-Protocol-Version` kommt im Quelltext genau einmal vor, in
einer CORS-Erlaubnisliste.

Vertrag im Kopf der Datei. Vier Punkte, die Handfassungen regelmaessig
verfehlen und die deshalb einzeln geprueft werden:

  * Der FEHLENDE Kopf ist zulaessig. Die sitzungslose Abkuerzung
    schickt ihn oft nicht, und sie traegt den meisten Verkehr - eine
    Ablehnung dort legt mehr lahm als sie schuetzt.
  * Die Ablehnung muss die unterstuetzten Fassungen NENNEN. Ohne die
    Liste kann die Gegenstelle nicht nachverhandeln, nur aufgeben.
  * Sie wirft nie. Zahl, Tabelle, Funktion - alles ergibt false. Ein
    Server, der an einem fremden Kopf stirbt, ist schlechter als einer,
    der ihn ignoriert.
  * 2026-07-28 wird abgelehnt, SOLANGE sie nicht in SUPPORTED steht.
    Wer die Zielfassung durchwinkt, bevor er sie spricht, hat den
    Fehler nur verschoben.

Unterscheidungsfaehig nachgewiesen, in beide Richtungen: Rueckgabe 0
gegen eine korrekte Fassung, Rueckgabe 1 mit fuenf Fehlschlaegen gegen
eine absichtlich kaputte.

Aufruf: lua5.4 tests/phase_b_acceptance.lua <impl.lua>
2026-08-08 08:23:08 +02:00

151 lines
6.2 KiB
Lua

-- Abnahmetest fuer Phase B der Angleichung an MCP 2026-07-28.
--
-- Phase A war beobachtend: feststellen, welche Fassungen ueberhaupt verlangt
-- werden. Phase B setzt durch. Gemessen am 2026-08-08 an hertz-tools:8080
-- antwortet lmcp mit HTTP 200 auf JEDE Fassungsangabe - auch auf 1999-01-01,
-- eine Fassung, die es nie gab. Es liest den Kopf schlicht nicht.
--
-- VERTRAG. Eine in sich geschlossene Lua-5.4-Datei, Tabelle M:
--
-- M.SUPPORTED Liste der Fassungen, die dieser Server spricht.
-- M.check(version) -> true, nil wenn zulaessig
-- -> false, <fehlertabelle> sonst
--
-- 1. version == nil oder "" -> zulaessig. Ein fehlender Kopf ist erlaubt;
-- der Server nimmt dann seine Grundfassung an. Das ist kein Sonderfall
-- aus Bequemlichkeit: die sitzungslose Abkuerzung schickt ihn oft nicht,
-- und sie traegt den meisten Verkehr.
-- 2. Genaue Uebereinstimmung mit einem Eintrag in M.SUPPORTED -> zulaessig.
-- 3. Alles andere -> false plus Fehlertabelle mit code == -32022 und einer
-- data.supported-Liste, die GENAU M.SUPPORTED entspricht. Ohne die Liste
-- kann die Gegenstelle nicht nachverhandeln, sie kann nur aufgeben.
-- 4. Wirft nie. Zahl, Tabelle, Wahrheitswert, Funktion - alles beantwortet
-- sie mit false, nicht mit einem Laufzeitfehler. Ein Server, der an
-- einem fremden Kopf stirbt, ist schlechter als einer, der ihn ignoriert.
-- 5. Aendert M.SUPPORTED nicht. Ein Aufruf darf die Liste des naechsten
-- nicht verschieben.
-- 6. Keine neue globale Variable.
--
-- Aufruf: lua5.4 phase_b_acceptance.lua <impl.lua>
local impl_path = arg and arg[1]
if not impl_path then
io.stderr:write("usage: lua5.4 phase_b_acceptance.lua <impl.lua>\n")
os.exit(2)
end
-- Globale Variablen VOR dem Laden festhalten, damit Regel 6 pruefbar ist.
local vorher = {}
for k in pairs(_G) do vorher[k] = true end
local chunk, lerr = loadfile(impl_path)
if not chunk then
io.stderr:write("kann " .. impl_path .. " nicht laden: " .. tostring(lerr) .. "\n")
os.exit(2)
end
local ok_load, M = pcall(chunk)
if not ok_load then
io.stderr:write("Laden warf: " .. tostring(M) .. "\n")
os.exit(2)
end
local fehler = 0
local function pruefe(name, bedingung, detail)
if bedingung then
print(string.format("[ok ] %s", name))
else
fehler = fehler + 1
print(string.format("[FEHLER] %s%s", name, detail and (" -> " .. tostring(detail)) or ""))
end
end
-- 0. Form
pruefe("M ist eine Tabelle", type(M) == "table", type(M))
if type(M) ~= "table" then print(fehler .. " Pruefungen fehlgeschlagen"); os.exit(1) end
pruefe("M.check ist aufrufbar", type(M.check) == "function", type(M.check))
pruefe("M.SUPPORTED ist eine nicht-leere Liste",
type(M.SUPPORTED) == "table" and #M.SUPPORTED >= 1, type(M.SUPPORTED))
if type(M.check) ~= "function" or type(M.SUPPORTED) ~= "table" then
print(fehler .. " Pruefungen fehlgeschlagen"); os.exit(1)
end
local function ruf(v)
local ok, a, b = pcall(M.check, v)
return ok, a, b
end
-- 1. fehlender Kopf
local ok, zulaessig = ruf(nil)
pruefe("nil ist zulaessig", ok and zulaessig == true, ok and tostring(zulaessig) or "warf")
ok, zulaessig = ruf("")
pruefe("leerer String ist zulaessig", ok and zulaessig == true, ok and tostring(zulaessig) or "warf")
-- 2. bekannte Fassung
local bekannt = M.SUPPORTED[1]
ok, zulaessig = ruf(bekannt)
pruefe("bekannte Fassung " .. tostring(bekannt) .. " ist zulaessig",
ok and zulaessig == true, ok and tostring(zulaessig) or "warf")
-- 3. unbekannte Fassung -> -32022 samt Liste
local ok3, zul3, err3 = ruf("1999-01-01")
pruefe("unbekannte Fassung wird abgelehnt", ok3 and zul3 == false,
ok3 and tostring(zul3) or "warf")
pruefe("Ablehnung traegt code -32022",
ok3 and type(err3) == "table" and err3.code == -32022,
ok3 and type(err3) == "table" and tostring(err3.code) or type(err3))
local liste_ok = false
if ok3 and type(err3) == "table" and type(err3.data) == "table"
and type(err3.data.supported) == "table" then
liste_ok = (#err3.data.supported == #M.SUPPORTED)
for i = 1, #M.SUPPORTED do
if err3.data.supported[i] ~= M.SUPPORTED[i] then liste_ok = false end
end
end
pruefe("Ablehnung nennt genau M.SUPPORTED", liste_ok)
-- 3b. die Fassung, auf die wir zuwandern, ist noch NICHT zulaessig.
-- Wer 2026-07-28 durchwinkt, bevor er sie spricht, hat den Fehler nur verschoben.
local ok3b, zul3b, err3b = ruf("2026-07-28")
local spricht_neu = false
for i = 1, #M.SUPPORTED do if M.SUPPORTED[i] == "2026-07-28" then spricht_neu = true end end
if spricht_neu then
pruefe("2026-07-28 steht in SUPPORTED und wird angenommen", ok3b and zul3b == true)
else
pruefe("2026-07-28 wird abgelehnt, solange sie nicht in SUPPORTED steht",
ok3b and zul3b == false and type(err3b) == "table" and err3b.code == -32022,
ok3b and tostring(zul3b) or "warf")
end
-- 4. wirft nie
local fremde = { 42, true, false, {}, print, 0/0 }
local alle_still, welcher = true, nil
for _, v in ipairs({42, true, {}, print}) do
local okx, zulx = pcall(M.check, v)
if not okx or zulx ~= false then alle_still = false; welcher = tostring(v) end
end
pruefe("fremde Typen ergeben false statt Laufzeitfehler", alle_still, welcher)
-- 5. SUPPORTED bleibt unangetastet
local kopie = {}
for i, v in ipairs(M.SUPPORTED) do kopie[i] = v end
ruf("1999-01-01"); ruf(bekannt); ruf(nil)
local unveraendert = (#kopie == #M.SUPPORTED)
for i = 1, #kopie do if kopie[i] ~= M.SUPPORTED[i] then unveraendert = false end end
pruefe("M.SUPPORTED wird durch Aufrufe nicht veraendert", unveraendert)
-- 5b. wiederholte Ablehnung bleibt gleich (kein verbrauchbarer Zustand)
local _, _, e1 = ruf("1999-01-01")
local _, _, e2 = ruf("1999-01-01")
pruefe("zweite Ablehnung ist so vollstaendig wie die erste",
type(e1) == "table" and type(e2) == "table"
and e1.code == e2.code
and type(e2.data) == "table" and type(e2.data.supported) == "table")
-- 6. keine neuen Globalen
local neu = {}
for k in pairs(_G) do if not vorher[k] then neu[#neu + 1] = tostring(k) end end
pruefe("keine neuen globalen Variablen", #neu == 0, table.concat(neu, ","))
print(fehler .. " Pruefungen fehlgeschlagen")
os.exit(fehler == 0 and 0 or 1)