From 4a3d401a404f3a82aac5389fd4cd8f5dbc4ffc39 Mon Sep 17 00:00:00 2001 From: TakalaWang Date: Tue, 6 Oct 2026 04:37:23 +0800 Subject: [PATCH] feat(runtime): accept runtime-bundle interactors Runner.interact rejected any interactor that was not a standalone Wasm module, although both sides already go through the same interactive preparation and CPython provides streaming fd 0. Drop the guard on the server runner and the browser runner Worker so Python interactors work, and cover it with a server integration test. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++++ docs/library-contract.md | 3 ++- src/runtime/runner.worker.ts | 3 --- src/server/judge.integration.test.ts | 40 ++++++++++++++++++++++++++++ src/server/server-runner.ts | 3 --- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d204d..23102e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to WASM-OJ are recorded here. Releases follow [Semantic Versioning](https://semver.org/) and the contract/package versioning policy in [the versioning policy](docs/versioning.md). +## Unreleased + +- Accept runtime-bundle interactors (for example CPython) in `Runner.interact` on the server + and in the browser runner Worker. Either side of an interactive session may now be a + standalone Wasm module or a runtime bundle that provides streaming fd 0. + ## 0.2.3 - 2026-10-05 - Download pinned browser toolchain assets before the compiler timeout starts, so slow diff --git a/docs/library-contract.md b/docs/library-contract.md index 6531588..214a5fa 100644 --- a/docs/library-contract.md +++ b/docs/library-contract.md @@ -241,7 +241,8 @@ set/multiset, output-file sets, and standalone sandboxed Wasm checkers. Interactive cases start contestant and interactor concurrently with full-duplex pipes, independent resource policies, process-local deterministic clocks, and secret inputs mounted only on the -interactor side. Runtime bundles that cannot provide streaming fd 0 are rejected for interaction. +interactor side. Either side may be a standalone Wasm module or a runtime bundle such as CPython; +runtime bundles that cannot provide streaming fd 0 are rejected for interaction. Each case executes under the broad hard policy once. Correct output and the same normalized metrics are evaluated against ordered cumulative `baseline`, `efficient`, and `optimal` policies. The diff --git a/src/runtime/runner.worker.ts b/src/runtime/runner.worker.ts index 6041f9b..ebeb2f8 100644 --- a/src/runtime/runner.worker.ts +++ b/src/runtime/runner.worker.ts @@ -412,9 +412,6 @@ async function interactArtifacts( ): Promise { const started = performance.now(); if (!runtimeDrivers) throw new Error("The WASM-OJ runtime-driver registry is not initialized."); - if (request.interactor.kind !== "wasm") { - throw new Error("Interactive judge artifacts must be standalone Wasm modules."); - } progress(request.requestId, "loading-toolchain", "Resolving contestant and interactor runtimes", 0.1); const [contestant, interactor] = await Promise.all([ prepareArtifactInteraction( diff --git a/src/server/judge.integration.test.ts b/src/server/judge.integration.test.ts index 5b7c69e..c333d74 100644 --- a/src/server/judge.integration.test.ts +++ b/src/server/judge.integration.test.ts @@ -130,6 +130,46 @@ describe.skipIf(!enabled)("real server judge contracts", () => { }); }); + it("runs a CPython interactor against a compiled contestant", { timeout: 300_000 }, async () => { + const contestant = await compileC("bundle-contestant", [ + "#include ", + "int main(void) {", + " int challenge = 0;", + " if (scanf(\"%d\", &challenge) != 1) return 2;", + " printf(\"%d\\n\", challenge + 1);", + " fflush(stdout);", + " return 0;", + "}", + ].join("\n")); + const built = await engine.compile({ + projectId: "judge-integration:python-interactor", + name: "python-interactor", + language: "python", + entry: "main.py", + files: { + "main.py": [ + "import sys", + "target = int(open(sys.argv[1]).read())", + "print(target - 1, flush=True)", + "sys.exit(0 if int(input()) == target else 1)", + ].join("\n") + "\n", + }, + }, { cache: false }); + expect(built.success).toBe(true); + expect(built.artifact?.kind).toBe("runtime-bundle"); + + const result = await engine.interact(contestant, built.artifact!, { + interactor: { args: ["/judge/input.txt"], files: { "/judge/input.txt": "42\n" } }, + }); + + expect(result).toMatchObject({ + contestantToInteractor: "42\n", + interactorToContestant: "41\n", + contestant: { code: 0, termination: "exited" }, + interactor: { code: 0, termination: "exited" }, + }); + }); + async function compileC(name: string, source: string): Promise { const entry = `src/${name}.c`; const input: CompileInput = { diff --git a/src/server/server-runner.ts b/src/server/server-runner.ts index 6eaa268..c8f0f35 100644 --- a/src/server/server-runner.ts +++ b/src/server/server-runner.ts @@ -303,9 +303,6 @@ export class ServerRunner implements Runner { if (this.activeOperation || this.cacheClearActive) { throw new Error("ServerRunner accepts one active operation at a time."); } - if (interactorArtifact.kind !== "wasm") { - throw new Error("Interactive judge artifacts must be standalone Wasm modules."); - } const operation = createServerRunOperation(this.generation); this.activeOperation = operation; this.inFlightRuns.add(operation);