"""Check that the shipping MASM tarot submission actually requests inference. Builds a test-only entry point; never edits production sources or binaries. Does not require model weights: even a missing-model error must be observable. """ from pathlib import Path import struct import subprocess import argparse from check_hall_expansion import ROOT, OUT, prepare from check_artifacts import toolchain def main(): parser = argparse.ArgumentParser() parser.add_argument('--with-model', action='store_true') args = parser.parse_args() prepare() source = (OUT / "module.asm").read_text(encoding="cp1251") entry = r""" .data fortuneAiProbePath db "build\hall-expansion\fortune-ai-probe.bin",0 fortuneAiProbeCount dd 0 fortuneAiProbeAnswerPath db "build\hall-expansion\fortune-ai-native-answer.bin",0 fortuneAiProbeStage dd 0 fortuneAiProbePagesPath db "build\hall-expansion\fortune-ai-pages.bin",0 fortuneAiProbePages dd 0 fortuneAiProbeShot db "build\hall-expansion\fortune-ai-stage-" fortuneAiProbeDigit db "0.bmp",0 fortuneAiProbeQuestion db QUESTION_BYTES,0 .code FortuneAiProbeStart PROC call BeginFortuneDialog3D invoke WindowEvents3D,0,102h,'1',0 lea esi,fortuneAiProbeQuestion fortune_ai_probe_input: movzx eax,BYTE PTR [esi] test eax,eax jz fortune_ai_probe_submit invoke WindowEvents3D,0,102h,eax,0 inc esi jmp fortune_ai_probe_input fortune_ai_probe_submit: call SubmitFortuneQuestion3D ; Pin the actual transmitted draw, before the ready frame is polled. mov fortuneDrawCards3D,0 mov fortuneDrawCards3D[4],23 mov fortuneDrawCards3D[8],17 invoke CreateFileA,OFFSET fortuneAiProbePath,40000000h,0,0,2,80h,0 mov ebx,eax invoke WriteFile,ebx,OFFSET fortuneWorkerState3D,4,OFFSET fortuneAiProbeCount,0 invoke WriteFile,ebx,OFFSET fortuneProcess3D,16,OFFSET fortuneAiProbeCount,0 invoke WriteFile,ebx,OFFSET fortuneRead3D,4,OFFSET fortuneAiProbeCount,0 invoke WriteFile,ebx,OFFSET fortuneWrite3D,4,OFFSET fortuneAiProbeCount,0 invoke FortuneCardSpeech3D,0 invoke WriteFile,ebx,OFFSET fortuneSpeech3D,256,OFFSET fortuneAiProbeCount,0 invoke CloseHandle,ebx ; MODEL_PROBE call StopFortuneWorker3D invoke ExitProcess,0 FortuneAiProbeStart ENDP end FortuneAiProbeStart """ question = 'Стоит ли мне начать учиться рисованию?' entry = entry.replace('QUESTION_BYTES', ','.join(map(str, question.encode('cp1251')))) if args.with_model: entry = entry.replace('; MODEL_PROBE', r''' mov esi,12000 fortune_ai_probe_wait: call PollFortuneWorker3D cmp fortuneWorkerState3D,5 je fortune_ai_probe_ready cmp fortuneWorkerState3D,4 je fortune_ai_probe_fail invoke Sleep,15 dec esi jnz fortune_ai_probe_wait fortune_ai_probe_fail: call StopFortuneWorker3D invoke ExitProcess,88 fortune_ai_probe_ready: invoke CreateFileA,OFFSET fortuneAiProbeAnswerPath,40000000h,0,0,2,80h,0 mov ebx,eax invoke WriteFile,ebx,OFFSET fortuneAnswer3D,fortuneAnswerLength3D,OFFSET fortuneAiProbeCount,0 invoke CloseHandle,ebx invoke CreateFileA,OFFSET fortuneAiProbePagesPath,40000000h,0,0,2,80h,0 mov fortuneAiProbePages,eax ; Run the real scene, including each page and explicit Continue input. fortune_ai_probe_scene: cmp fortunePhase3D,12 je fortune_ai_probe_done cmp fortuneReadingShown3D,1 jne fortune_ai_probe_tick mov fortuneSpeechTime3D,20000 call RenderFortuneDialog3D cmp fortuneSpeechHeight3D,140 ja fortune_ai_probe_fail cmp fortuneSpeechWidth3D,204 ja fortune_ai_probe_fail mov eax,fortuneSection3D mov fortuneAiProbeStage,eax add al,'0' mov fortuneAiProbeDigit,al invoke ExpansionSnapshot,OFFSET fortuneAiProbeShot invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSection3D,4,OFFSET fortuneAiProbeCount,0 invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSpeechLength3D,4,OFFSET fortuneAiProbeCount,0 invoke WriteFile,fortuneAiProbePages,OFFSET fortuneSpeech3D,fortuneSpeechLength3D,OFFSET fortuneAiProbeCount,0 mov mouseX3D,20 mov mouseY3D,232 mov mouseClick3D,1 call HandleFortuneDialog3D fortune_ai_probe_tick: add animationClock3D,100 call UpdateFortuneSession3D jmp fortune_ai_probe_scene fortune_ai_probe_done: invoke CloseHandle,fortuneAiProbePages cmp fortuneRevealed3D,3 jne fortune_ai_probe_fail cmp fortuneAiProbeStage,3 jne fortune_ai_probe_fail ''') assert source.count("end ExpansionTestStart") == 1 source = source.replace("end ExpansionTestStart", 'include tools/tests/fortune_ai_test.inc\n' + entry) source = source.replace(' invoke ExitProcess,0\nFortuneAiProbeStart ENDP', ' call FortuneAiNativeTests\n invoke ExitProcess,0\nFortuneAiProbeStart ENDP') stem = "fortune-ai-probe" (OUT / (stem + ".asm")).write_text(source, encoding="cp1251") assembler, linker = toolchain() subprocess.run([str(assembler), "/c", "/coff", "/Cp", "/nologo", "/I.", f"/Fobuild/hall-expansion/{stem}.obj", f"build/hall-expansion/{stem}.asm"], cwd=ROOT, check=True) sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1] subprocess.run([str(linker), "/nologo", "/SUBSYSTEM:CONSOLE", "/ENTRY:FortuneAiProbeStart@0", f"/LIBPATH:{sdk}", f"/OUT:build/hall-expansion/{stem}.exe", f"build/hall-expansion/{stem}.obj", "build/hall-expansion/helpers.obj", "kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib", "ws2_32.lib"], cwd=ROOT, check=True) subprocess.run([str(OUT / (stem + ".exe"))], cwd=ROOT, timeout=210 if args.with_model else 30, check=True) raw = (OUT / "fortune-ai-probe.bin").read_bytes() state, process, thread, pid, tid, reader, writer = struct.unpack_from("<7I", raw) speech = raw[28:].split(b"\0", 1)[0].decode("cp1251") print(f"Native submit: state={state}, process={process}, reader={reader}, writer={writer}") print("Card speech:", ascii(speech)) assert (state == 1 and process and reader and writer) or (state == 4 and not process), ( "Tarot submission neither starts/queues inference nor reports an AI error; " "the actual native scene cannot produce a model reading." ) if args.with_model: from check_fortune_ai import check_reading text = (OUT / 'fortune-ai-native-answer.bin').read_bytes().decode('cp1251').replace('\0', '\n') check_reading(text, [0, 23, 17]) pages = (OUT / 'fortune-ai-pages.bin').read_bytes() recovered = [[] for _ in range(4)] offset = 0 while offset < len(pages): section, size = struct.unpack_from(' worker -> model -> four paged scene stages') print("PASS: native submit, partial IPC frames, bounded parser/paging, timeout and cleanup") if __name__ == "__main__": main()