2424
2525ROOT = Path (__file__ ).resolve ().parents [1 ]
2626STORAGE_KEY = "codaro-custom-curricula"
27- FIXTURE_TITLE = "Runtime recovery browser fixture"
27+ PACKAGE_FAILURE_TITLE = "Runtime package recovery fixture"
28+ CELL_FAILURE_TITLE = "Runtime cell failure fixture"
2829MISSING_PACKAGE = "codaro_missing_runtime_pkg"
30+ CELL_FAILURE_SYMBOL = "undefined_runtime_value"
2931
3032
3133def main (argv : list [str ] | None = None ) -> int :
@@ -51,17 +53,25 @@ def main(argv: list[str] | None = None) -> int:
5153 cli = PlaywrightCli (cliPath = cliPath , cwd = tempRoot , session = session )
5254 cli .run ("open" , url )
5355 cli .run ("resize" , "1280" , "900" )
54- cli .run ("localstorage-set" , STORAGE_KEY , json .dumps ([customCurriculumEntry ()], ensure_ascii = False ))
56+ cli .run ("localstorage-set" , STORAGE_KEY , json .dumps ([
57+ customCurriculumEntry (PACKAGE_FAILURE_TITLE , customCurriculumDocument ("package-failure" )),
58+ customCurriculumEntry (CELL_FAILURE_TITLE , customCurriculumDocument ("cell-failure" )),
59+ ], ensure_ascii = False ))
5560 cli .run ("reload" )
56- cli .waitEval (jsTextPresent (FIXTURE_TITLE ), "custom runtime recovery curriculum" )
57- cli .eval (jsClickText (FIXTURE_TITLE ))
61+ cli .waitEval (jsTextPresent (PACKAGE_FAILURE_TITLE ), "custom runtime recovery curriculum" )
62+ cli .eval (jsClickText (PACKAGE_FAILURE_TITLE ))
5863 cli .waitEval (jsTextPresent ("셀을 실행하면 결과와 오류가 여기에 표시됩니다." ), "idle result recovery copy" )
5964 idle = cli .eval (jsAssertIdleRuntimeRecovery ())
6065 cli .eval (jsClickFirstRunButton ())
6166 cli .waitEval (jsTextPresent ("라이브러리 준비 실패" ), "package install failure notice" )
6267 failure = cli .eval (jsAssertPackageFailureRecovery ())
68+ cli .eval (jsClickText (CELL_FAILURE_TITLE ))
69+ cli .waitEval (jsTextPresent ("셀을 실행하면 결과와 오류가 여기에 표시됩니다." ), "cell failure idle result copy" )
70+ cli .eval (jsClickFirstRunButton ())
71+ cli .waitEval (jsTextPresent ("셀 실행 실패" ), "cell execution failure recovery" )
72+ cellFailure = cli .eval (jsAssertCellFailureRecovery ())
6373 api .assertExpectedCalls ()
64- print (f"ok: runtime recovery browser verified { idle } { failure } " )
74+ print (f"ok: runtime recovery browser verified { idle } { failure } { cellFailure } " )
6575 return 0
6676 except (VerificationError , PlaywrightCliError ) as exc :
6777 print (f"FAIL: { exc } " , file = sys .stderr )
@@ -108,11 +118,12 @@ def assertExpectedCalls(self) -> None:
108118 "POST /api/kernel/create" ,
109119 "GET /api/kernel/session-runtime-recovery/packages/list packages-check" ,
110120 f"POST /api/kernel/session-runtime-recovery/packages/install packages-install { MISSING_PACKAGE } " ,
121+ "POST /api/kernel/session-runtime-recovery/execute cell-call cell-failure" ,
111122 }
112123 missing = sorted (required - set (self .calls ))
113124 if missing :
114125 raise VerificationError ("runtime recovery API calls missing: " + ", " .join (missing ))
115- if any ("execute " in call for call in self .calls ):
126+ if any ("cell-call package-failure " in call for call in self .calls ):
116127 raise VerificationError ("cell-call endpoint should not run after package install failure" )
117128
118129 def _handler (self ):
@@ -138,9 +149,9 @@ def do_GET(self):
138149 "learningPaths" : {},
139150 })
140151 elif path .startswith ("/api/curriculum/contents/" ):
141- self ._sendJson ({"category" : "30days" , "categoryName" : "파이썬 기초" , "contents" : [{"contentId" : "runtime-recovery" , "title" : FIXTURE_TITLE }]})
152+ self ._sendJson ({"category" : "30days" , "categoryName" : "파이썬 기초" , "contents" : [{"contentId" : "runtime-recovery" , "title" : PACKAGE_FAILURE_TITLE }]})
142153 elif path .startswith ("/api/curriculum/content/" ):
143- self ._sendJson ({"document" : customCurriculumDocument (), "solutions" : {}})
154+ self ._sendJson ({"document" : customCurriculumDocument ("package-failure" ), "solutions" : {}})
144155 elif path == "/api/ai/tools" :
145156 self ._sendJson ({"groups" : [], "lanes" : [], "tools" : [], "grouped" : {}, "byLane" : {}})
146157 elif path == "/api/ai/profile" :
@@ -177,8 +188,22 @@ def do_POST(self):
177188 "skipped" : False ,
178189 })
179190 elif path .endswith ("/execute" ):
180- owner .calls .append (f"POST { path } cell-call" )
181- self ._sendJson ({"type" : "text" , "status" : "error" , "data" : "should not execute" , "blockId" : payload .get ("blockId" )})
191+ code = str (payload .get ("code" ) or "" )
192+ if MISSING_PACKAGE in code :
193+ owner .calls .append (f"POST { path } cell-call package-failure" )
194+ else :
195+ owner .calls .append (f"POST { path } cell-call cell-failure" )
196+ self ._sendJson ({
197+ "type" : "error" ,
198+ "status" : "error" ,
199+ "data" : f"NameError: name '{ CELL_FAILURE_SYMBOL } ' is not defined" ,
200+ "stdout" : "" ,
201+ "stderr" : f"Traceback (most recent call last):\n NameError: name '{ CELL_FAILURE_SYMBOL } ' is not defined" ,
202+ "blockId" : payload .get ("blockId" ),
203+ "variables" : [],
204+ "stateDelta" : {"added" : [], "updated" : [], "removed" : []},
205+ "executionCount" : 1 ,
206+ })
182207 else :
183208 self ._sendJson ({"error" : f"unhandled { path } " }, status = 404 )
184209
@@ -298,6 +323,21 @@ def jsAssertPackageFailureRecovery() -> str:
298323""" )
299324
300325
326+ def jsAssertCellFailureRecovery () -> str :
327+ return compactJs (f"""
328+ (() => {{
329+ const text = document.body.innerText;
330+ const required = ['셀 실행 실패', '{ CELL_FAILURE_SYMBOL } ', '오류 메시지의 마지막 줄부터 확인', '다시 실행할 수 있습니다'];
331+ const missing = required.filter((item) => !text.includes(item));
332+ if (missing.length) throw new Error('cell failure recovery missing: ' + missing.join(', '));
333+ if (!document.querySelector('[data-runtime-recovery="cell-error"]')) {{
334+ throw new Error('cell failure recovery hint missing near execution output');
335+ }}
336+ return 'cell-failure-recovery-ok';
337+ }})()
338+ """ )
339+
340+
301341def debugPageState (cli : PlaywrightCli ) -> str :
302342 try :
303343 return cli .eval ("document.body.innerText.slice(0, 2000)" )
@@ -309,17 +349,17 @@ def compactJs(source: str) -> str:
309349 return " " .join (line .strip () for line in source .strip ().splitlines () if line .strip ())
310350
311351
312- def customCurriculumEntry () -> dict [str , Any ]:
352+ def customCurriculumEntry (title : str , document : dict [ str , Any ] ) -> dict [str , Any ]:
313353 return {
314- "id" : "custom-runtime-recovery " ,
315- "title" : FIXTURE_TITLE ,
316- "document" : customCurriculumDocument () ,
354+ "id" : f "custom-{ title . lower (). replace ( ' ' , '-' ) } " ,
355+ "title" : title ,
356+ "document" : document ,
317357 "createdAt" : 1_700_000_000_000 ,
318358 }
319359
320360
321- def customCurriculumDocument () -> dict [str , Any ]:
322- document , _solutions = yamlToDocument (yaml .safe_load (structuredLessonYaml ()), "runtime-recovery" , "package-failure" )
361+ def customCurriculumDocument (caseId : str ) -> dict [str , Any ]:
362+ document , _solutions = yamlToDocument (yaml .safe_load (structuredLessonYaml (caseId )), "runtime-recovery" , caseId )
323363 payload = document .model_dump (mode = "json" )
324364 payload ["id" ] = "runtime-recovery-document"
325365 return compactDocumentForBrowserStorage (payload )
@@ -416,14 +456,27 @@ def compactSectionContract(contract: dict[str, Any]) -> dict[str, Any]:
416456 }
417457
418458
419- def structuredLessonYaml () -> str :
459+ def structuredLessonYaml (caseId : str ) -> str :
460+ if caseId == "cell-failure" :
461+ title = CELL_FAILURE_TITLE
462+ packages = " []"
463+ snippet = f"print({ CELL_FAILURE_SYMBOL } )"
464+ starterCode = f"print({ CELL_FAILURE_SYMBOL } )"
465+ subtitle = "셀 실행 오류를 결과 근처에서 확인"
466+ why = "실행 오류가 package failure와 섞이지 않아야 코드를 고쳐 다시 실행하기 쉽다."
467+ else :
468+ title = PACKAGE_FAILURE_TITLE
469+ packages = f"\n - { MISSING_PACKAGE } "
470+ snippet = f"import { MISSING_PACKAGE } \n print(\" ready\" )"
471+ starterCode = f"import { MISSING_PACKAGE } \n print(\" ready\" )"
472+ subtitle = "패키지 설치 실패를 셀 근처에서 확인"
473+ why = "설치 실패가 cell-call로 번지지 않아야 다시 실행 경로가 분명하다."
420474 return f"""
421475meta:
422- title: { FIXTURE_TITLE }
476+ title: { title }
423477 audience: local dogfood learner
424478 difficulty: beginner
425- packages:
426- - { MISSING_PACKAGE }
479+ packages:{ packages }
427480intro:
428481 direction: runtime recovery path
429482 benefits:
@@ -433,21 +486,19 @@ def structuredLessonYaml() -> str:
433486sections:
434487 - id: recovery
435488 title: Runtime 복구
436- subtitle: 패키지 설치 실패를 셀 근처에서 확인
489+ subtitle: { subtitle }
437490 goal: runtime preflight가 실행 전에 필요한 패키지를 확인한다.
438- why: 설치 실패가 cell-call로 번지지 않아야 다시 실행 경로가 분명하다.
491+ why: { why }
439492 explanation: 실행 전 package preflight가 필요한 패키지를 확인하고 실패를 사용자 문장으로 돌려준다.
440493 tips:
441494 - 실패한 패키지 이름과 설치 경로를 먼저 본다.
442495 - 설치 실패 뒤에는 셀 실행을 진행하지 않는다.
443496 snippet: |
444- import { MISSING_PACKAGE }
445- print("ready")
497+ { indentYamlBlock (snippet , 6 )}
446498 exercise:
447499 prompt: 실패를 확인하기 위해 실습 셀을 실행하세요.
448500 starterCode: |
449- import { MISSING_PACKAGE }
450- print("ready")
501+ { indentYamlBlock (starterCode , 8 )}
451502 solution: |
452503 print("fallback")
453504 check:
@@ -459,5 +510,10 @@ def structuredLessonYaml() -> str:
459510"""
460511
461512
513+ def indentYamlBlock (value : str , spaces : int ) -> str :
514+ prefix = " " * spaces
515+ return "\n " .join (f"{ prefix } { line } " for line in value .splitlines ())
516+
517+
462518if __name__ == "__main__" :
463519 raise SystemExit (main ())
0 commit comments