[{"data":1,"prerenderedAt":465},["ShallowReactive",2],{"blog-de-der-test-der-produktivdaten-zerstoerte":3,"header-blog-translations-/de/blog/der-test-der-produktivdaten-zerstoerte":462},{"id":4,"title":5,"author":6,"body":7,"date":445,"description":446,"draft":447,"extension":448,"image":449,"meta":450,"navigation":451,"path":452,"seo":453,"stem":454,"tags":455,"translationKey":460,"__hash__":461},"blog_de/blog/de/der-test-der-produktivdaten-zerstoerte.md","Der Test, der Produktivdaten zerstörte","Patrick Hofmann",{"type":8,"value":9,"toc":438},"minimark",[10,19,22,27,33,36,130,133,137,152,261,272,276,282,297,303,307,310,321,418,424,428,431,434],[11,12,13,14,18],"p",{},"Es gibt in der Nest-Codebase einen vitest-Case, der absichtlich kaputtes JSON schreibt. Er nimmt den Pfad, an dem die Agent-Registry liegt, schreibt ",[15,16,17],"code",{},"{not json"," hinein und prüft dann: verkraftet der Code das? Fällt er auf einen leeren Default zurück statt zu crashen? Das ist ein guter Test. Robustheit gegen korrupte Persistenz ist genau die Art von Ding, die man explizit absichern will, weil sie im Normalbetrieb nie auftritt und dann irgendwann doch.",[11,20,21],{},"Der Test hat lange genau das getan, was er sollte. Bis er anfing, die echten Produktivdaten zu zerstören.",[23,24,26],"h2",{"id":25},"wie-es-vorher-war","Wie es vorher war",[11,28,29,32],{},[15,30,31],{},"resolveRegistryPath()"," hatte eine einfache Reihenfolge: gibt es eine Environment-Variable, die den Pfad setzt, nimm die. Sonst fall auf einen Default zurück. Der Test setzte die Variable auf einen Pfad in seinem eigenen Temp-Verzeichnis, schrieb dort Müll hinein, ließ den Code drüberlaufen, und räumte am Ende auf. Ein geschlossener Kreis. Der Test fasste nur an, was ihm gehörte.",[11,34,35],{},"So sah das im Kern aus:",[37,38,43],"pre",{"className":39,"code":40,"language":41,"meta":42,"style":42},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","function resolveRegistryPath() {\n  if (process.env.REGISTRY_PATH) return process.env.REGISTRY_PATH;\n  return defaultRegistryPath();\n}\n","js","",[15,44,45,65,111,124],{"__ignoreMap":42},[46,47,50,54,58,62],"span",{"class":48,"line":49},"line",1,[46,51,53],{"class":52},"spNyl","function",[46,55,57],{"class":56},"s2Zo4"," resolveRegistryPath",[46,59,61],{"class":60},"sMK4o","()",[46,63,64],{"class":60}," {\n",[46,66,68,72,76,80,83,86,88,91,94,97,100,102,104,106,108],{"class":48,"line":67},2,[46,69,71],{"class":70},"s7zQu","  if",[46,73,75],{"class":74},"swJcz"," (",[46,77,79],{"class":78},"sTEyZ","process",[46,81,82],{"class":60},".",[46,84,85],{"class":78},"env",[46,87,82],{"class":60},[46,89,90],{"class":78},"REGISTRY_PATH",[46,92,93],{"class":74},") ",[46,95,96],{"class":70},"return",[46,98,99],{"class":78}," process",[46,101,82],{"class":60},[46,103,85],{"class":78},[46,105,82],{"class":60},[46,107,90],{"class":78},[46,109,110],{"class":60},";\n",[46,112,114,117,120,122],{"class":48,"line":113},3,[46,115,116],{"class":70},"  return",[46,118,119],{"class":56}," defaultRegistryPath",[46,121,61],{"class":74},[46,123,110],{"class":60},[46,125,127],{"class":48,"line":126},4,[46,128,129],{"class":60},"}\n",[11,131,132],{},"Nichts Spektakuläres. Env zuerst, Default danach. Die Test-Sicherheit lag implizit in dieser Reihenfolge: weil das Env-Override ganz oben stand, konnte der Test sicher sein, dass sein gesetzter Pfad gewinnt.",[23,134,136],{"id":135},"warum-ich-es-geändert-habe","Warum ich es geändert habe",[11,138,139,140,143,144,147,148,151],{},"Der Nest-Daemon brauchte einen festen Ort für ",[15,141,142],{},"agents.json"," — ",[15,145,146],{},"/var/openape/nest/agents.json",", weil der Daemon als eigener Service-User mit eigenem ",[15,149,150],{},"HOME"," läuft und sich nicht auf das verlassen kann, was zufällig im Environment des aufrufenden Prozesses steht. Also kam oben in den Resolver ein Check: wenn die kanonische Datei existiert, nimm die.",[37,153,155],{"className":39,"code":154,"language":41,"meta":42,"style":42},"function resolveRegistryPath() {\n  if (existsSync(\"/var/openape/nest/agents.json\")) {\n    return \"/var/openape/nest/agents.json\";\n  }\n  if (process.env.REGISTRY_PATH) return process.env.REGISTRY_PATH;\n  return defaultRegistryPath();\n}\n",[15,156,157,167,193,207,212,245,256],{"__ignoreMap":42},[46,158,159,161,163,165],{"class":48,"line":49},[46,160,53],{"class":52},[46,162,57],{"class":56},[46,164,61],{"class":60},[46,166,64],{"class":60},[46,168,169,171,173,176,179,182,185,187,190],{"class":48,"line":67},[46,170,71],{"class":70},[46,172,75],{"class":74},[46,174,175],{"class":56},"existsSync",[46,177,178],{"class":74},"(",[46,180,181],{"class":60},"\"",[46,183,146],{"class":184},"sfazB",[46,186,181],{"class":60},[46,188,189],{"class":74},")) ",[46,191,192],{"class":60},"{\n",[46,194,195,198,201,203,205],{"class":48,"line":113},[46,196,197],{"class":70},"    return",[46,199,200],{"class":60}," \"",[46,202,146],{"class":184},[46,204,181],{"class":60},[46,206,110],{"class":60},[46,208,209],{"class":48,"line":126},[46,210,211],{"class":60},"  }\n",[46,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243],{"class":48,"line":214},5,[46,216,71],{"class":70},[46,218,75],{"class":74},[46,220,79],{"class":78},[46,222,82],{"class":60},[46,224,85],{"class":78},[46,226,82],{"class":60},[46,228,90],{"class":78},[46,230,93],{"class":74},[46,232,96],{"class":70},[46,234,99],{"class":78},[46,236,82],{"class":60},[46,238,85],{"class":78},[46,240,82],{"class":60},[46,242,90],{"class":78},[46,244,110],{"class":60},[46,246,248,250,252,254],{"class":48,"line":247},6,[46,249,116],{"class":70},[46,251,119],{"class":56},[46,253,61],{"class":74},[46,255,110],{"class":60},[46,257,259],{"class":48,"line":258},7,[46,260,129],{"class":60},[11,262,263,264,266,267,271],{},"Für den Daemon war das richtig. Er findet seine Registry verlässlich, egal wer ihn startet. Was ich nicht mitgedacht habe: dieser ",[15,265,175],{}," sitzt ",[268,269,270],"em",{},"über"," dem Env-Check.",[23,273,275],{"id":274},"wie-es-jetzt-aussieht","Wie es jetzt aussieht",[11,277,278,279,281],{},"Auf meiner Dev-Box lief der Nest-Daemon. Er hatte ",[15,280,146],{}," längst angelegt. Die Datei existierte also.",[11,283,284,285,287,288,290,291,293,294,296],{},"Dann lief der Test. Er setzte brav ",[15,286,90],{}," auf sein Temp-Verzeichnis, fragte ",[15,289,31],{}," wo die Registry liegt — und bekam ",[15,292,146],{}," zurück. Nicht seinen Temp-Pfad. Den echten. Weil der ",[15,295,175],{},"-Zweig vor dem Env-Zweig steht und auf einer Dev-Box, auf der der Daemon schon mal gelaufen ist, immer trifft.",[11,298,299,300,302],{},"Der Test schrieb sein ",[15,301,17],{}," also nicht in sein Temp-Verzeichnis. Er schrieb es in die Registry, die der laufende Daemon liest. Ein Test, der beweisen sollte, dass der Code korrupte Daten übersteht, hat die Daten korrupt gemacht — produktiv, auf einer Maschine, auf der echte Agents registriert waren.",[23,304,306],{"id":305},"was-weggefallen-ist","Was weggefallen ist",[11,308,309],{},"Die Annahme, dass ein Test nur anfasst, was ihm gehört. Diese Annahme war nie im Code verankert. Sie hing daran, dass das Env-Override zufällig oben im Resolver stand. Solange das so war, war die Annahme wahr. In dem Moment, in dem ein neuer Zweig darüber kam, war sie es nicht mehr — und nichts hat geschrien, weil eine Reihenfolge keine Signatur hat, die ein Typchecker prüfen könnte.",[11,311,312,313,316,317,320],{},"Der Fix ist klein: Env-Override ganz nach oben, vor jedes Filesystem-Probing. Und im Test-",[15,314,315],{},"setUp"," verpflichtend — der Test prüft, dass das Override gesetzt ist, ",[268,318,319],{},"bevor"," er irgendetwas schreibt. Kein gesetztes Override, kein Schreibzugriff.",[37,322,324],{"className":39,"code":323,"language":41,"meta":42,"style":42},"function resolveRegistryPath() {\n  if (process.env.REGISTRY_PATH) return process.env.REGISTRY_PATH;\n  if (existsSync(\"/var/openape/nest/agents.json\")) {\n    return \"/var/openape/nest/agents.json\";\n  }\n  return defaultRegistryPath();\n}\n",[15,325,326,336,368,388,400,404,414],{"__ignoreMap":42},[46,327,328,330,332,334],{"class":48,"line":49},[46,329,53],{"class":52},[46,331,57],{"class":56},[46,333,61],{"class":60},[46,335,64],{"class":60},[46,337,338,340,342,344,346,348,350,352,354,356,358,360,362,364,366],{"class":48,"line":67},[46,339,71],{"class":70},[46,341,75],{"class":74},[46,343,79],{"class":78},[46,345,82],{"class":60},[46,347,85],{"class":78},[46,349,82],{"class":60},[46,351,90],{"class":78},[46,353,93],{"class":74},[46,355,96],{"class":70},[46,357,99],{"class":78},[46,359,82],{"class":60},[46,361,85],{"class":78},[46,363,82],{"class":60},[46,365,90],{"class":78},[46,367,110],{"class":60},[46,369,370,372,374,376,378,380,382,384,386],{"class":48,"line":113},[46,371,71],{"class":70},[46,373,75],{"class":74},[46,375,175],{"class":56},[46,377,178],{"class":74},[46,379,181],{"class":60},[46,381,146],{"class":184},[46,383,181],{"class":60},[46,385,189],{"class":74},[46,387,192],{"class":60},[46,389,390,392,394,396,398],{"class":48,"line":126},[46,391,197],{"class":70},[46,393,200],{"class":60},[46,395,146],{"class":184},[46,397,181],{"class":60},[46,399,110],{"class":60},[46,401,402],{"class":48,"line":214},[46,403,211],{"class":60},[46,405,406,408,410,412],{"class":48,"line":247},[46,407,116],{"class":70},[46,409,119],{"class":56},[46,411,61],{"class":74},[46,413,110],{"class":60},[46,415,416],{"class":48,"line":258},[46,417,129],{"class":60},[11,419,420,421,423],{},"Diese Guard hätte im selben Commit kommen müssen wie der ",[15,422,175],{},"-Zweig. Nicht als Nachtrag, nachdem die Box einmal Daten verloren hat. Wer einen Zweig oben in einen Pfad-Resolver einzieht, ändert die Präzedenz für alles, was durch diesen Resolver schreibt — und das schließt jeden Test ein, der je darüber geschrieben hat.",[23,425,427],{"id":426},"die-reihenfolge-ist-der-vertrag","Die Reihenfolge ist der Vertrag",[11,429,430],{},"Ich hatte den Resolver als Implementierungsdetail behandelt. Er bestimmt einen Pfad, das Verhalten ist offensichtlich, da gibt es nichts zu dokumentieren. Falsch. Die Präzedenz in einem Pfad-Resolver ist kein Detail, sie ist eine API. Jeder Aufrufer — und der lauteste Aufrufer war hier der Test, der absichtlich destruktiv schreibt — verlässt sich auf eine bestimmte Reihenfolge, auch wenn niemand sie irgendwo aufschreibt.",[11,432,433],{},"Ein Robustheits-Test ist nur so sicher wie die Präzedenz, der er vertraut. Der Test war korrekt. Der Resolver war korrekt. Beide für sich genommen taten genau das, was sie sollten. Kaputt war nur die Reihenfolge dazwischen — und die gehörte niemandem, also hat sie auch niemand geprüft.",[435,436,437],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":42,"searchDepth":67,"depth":67,"links":439},[440,441,442,443,444],{"id":25,"depth":67,"text":26},{"id":135,"depth":67,"text":136},{"id":274,"depth":67,"text":275},{"id":305,"depth":67,"text":306},{"id":426,"depth":67,"text":427},"2026-05-13","Ein vitest-Case schrieb absichtlich kaputtes JSON, um zu prüfen ob der Code es verkraftet. Das ging gut — bis eine Zeile existsSync oben im Resolver die Reihenfolge kippte und derselbe Test in echte Produktivdaten schrieb.",false,"md",null,{},true,"/blog/de/der-test-der-produktivdaten-zerstoerte",{"title":5,"description":446},"blog/de/der-test-der-produktivdaten-zerstoerte",[456,457,458,459],"Testing","Infrastructure","Building in Public","OpenApe","test-that-destroyed-prod-data","Xqm6gwr4A-MZXy599LUX0CP50ts6YNuH1aEc-LOpyfQ",{"de":463,"en":464},"/de/blog/der-test-der-produktivdaten-zerstoerte","/en/blog/test-that-destroyed-prod-data",1779001885471]