e44b35b0e0
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>
151 lines
6.2 KiB
Lua
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)
|