Skip to content

Commit bc77eb2

Browse files
committed
CI: WASI와 무자산 머신 레인을 분리
Web Machine 러너를 무자산, WASI, v86 레인으로 명시적으로 나눴다. Ubuntu 소비 job은 WASI 자산을 준비하고 Windows Edge는 무자산 계약만 실행한다. WASI 브라우저 페이지에 공통 RuntimeContract와 capability 검사 네 개를 추가했다. 기존 13개 하한을 낮추지 않고 실제 공개 계약으로 채워 검사 사각지대를 닫았다. 검증: npm test 3154/3154, WASI 14/14, Web Machine 7/7과 8/8 통과.
1 parent 58832e9 commit bc77eb2

5 files changed

Lines changed: 32 additions & 10 deletions

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ jobs:
7878
- run: node tests/browser/run.mjs tests/browser/wasiGate.html
7979
env:
8080
PYPROC_BROWSER: /usr/bin/google-chrome
81-
# Web Machine probe: host 계약, 3-guest 계약(WASI 어댑터 포함 = 위 자산 필요),
82-
# owner 승계 경쟁. x86 자산이 필요한 레인은 npm run test:web-machine:v86으로 로컬에서 돈다.
83-
- run: npm run test:web-machine
81+
# Web Machine probe: 무자산 계약에 실제 WASI guest 레인을 더한다.
82+
# x86 자산이 필요한 레인은 별도 job이 소유한다.
83+
- run: npm run test:web-machine:wasi
8484
env:
8585
PYPROC_BROWSER: /usr/bin/google-chrome
8686
# 소켓 레인: 이 표면은 "릴레이를 배송하지 않는다"는 이유로 오래 무게이트였다. 러너가
@@ -95,7 +95,7 @@ jobs:
9595

9696
# 실제 Microsoft Edge의 Windows 배포 경계. 설치 tarball 소비자/골든 경로와 핵심 브라우저
9797
# 계약을 Chrome과 독립해 문다. examples는 Chrome의 동일 쇼케이스를 그대로 반복해 비용이
98-
# 커서 제외하고, WASI/v86 실행 자산 레인은 아래 Ubuntu 전용 job이 계속 소유한다.
98+
# 커서 제외하고, WASI/v86 실행 자산 레인은 Ubuntu 전용 job이 계속 소유한다.
9999
edge-release:
100100
runs-on: windows-latest
101101
env:
@@ -121,7 +121,7 @@ jobs:
121121
# npm pack 설치물의 public specifier와 골든 워크플로를 실제 Edge에서 검증한다.
122122
- run: npm run test:consumer
123123
- run: npm run test:golden
124-
# 무자산 Web Machine 계약만 실행한다. v86/WASI 자산 준비는 의도적으로 하지 않는다.
124+
# 무자산 Web Machine 계약만 실행한다. 실제 WASI/v86 guest는 Ubuntu 레인이 소유한다.
125125
- run: npm run test:web-machine
126126
# 로컬 relay/TCP만 쓰는 통합면은 추가 다운로드 없이 Edge 차이를 잡을 수 있어 함께 돈다.
127127
- run: npm run test:socket
@@ -163,8 +163,12 @@ jobs:
163163
node-version: 22
164164
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
165165
with:
166-
path: tests/webMachine/fixtures/v86/assets
166+
path: |
167+
tests/webMachine/fixtures/v86/assets
168+
tests/attempts/enginePort/python-3.14.6.wasm
169+
tests/attempts/enginePort/python314-stdlib.zip
167170
key: web-machine-v86-assets-${{ hashFiles('scripts/assetCatalog.json') }}
171+
- run: node scripts/fetchWasiAssets.mjs
168172
# 실 x86 Linux guest를 부팅해 dual-boot과 장치(디스플레이·프레임버퍼·패킷·포인터)를 문다.
169173
# 타임아웃을 올린 이유는 공유 러너에서 v86 부팅이 느리기 때문이다(자산이 아니라 CPU 몫).
170174
- run: npm run test:web-machine:v86

docs/consuming/capabilityMatrix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ A runnable surface is an example a person opens, a browser gate, a probe, or a t
5858
| Immortal Python Machine | Many tabs share one Python state, and execution continues from the last commit even after the leader tab is force-killed and after every tab closes | `open({ persistent })` (formerly `openPersistentMachine`) -> a `KernelElection` handle; the journal is the durable verb set on `machine.history` (`MachineJournal`) | Experimental | Web Locks, BroadcastChannel, OPFS, and the same origin. Using the SAB-based OS capabilities on top also needs `crossOriginIsolated` and JSPI | [immortal demo](../../examples/immortal.html), [kernel election probe](../../tests/attempts/pythonMachine/kernelElectionProbe.html), [installed-package consumer gate](../../tests/browser/productConsumer.mjs), [golden workflow](../../tests/browser/goldenWorkflow.mjs) | `npm test` (the [election protocol] section: reject state machine, epoch fencing, outcome records, conditional resend, unsafe-heap refusal, served-cache idempotence, all with negative tests confirmed), `npm run test:consumer`, `npm run test:golden` | Not a multi-user server. Recovery is to a completed commit boundary. The [durable RPC state table](contract.md#durable-rpc-state-table-normative) is the retry boundary: a normal follower cannot prove the leader heap portable, so a sent call cut off by failover is outcome-unknown and is not resent |
5959
| Machine container and jobs | Starts a separate machine kernel inside a browser worker and offers job control | `machine.containers()` -> `MachineContainer`, `machine.jobs()` -> `JobControl` (a process-OS lane: it assumes the same replay pool as `machine.proc`) | Experimental | `crossOriginIsolated`, the worker graph, and a product process-lifecycle policy | [browser gate](../../tests/browser/gate.html) (the machine.jobs and machine.containers evidence checks), [product consumer gate](../../tests/browser/productConsumer.mjs), [machine container probe](../../tests/attempts/pythonMachine/machineContainerProbe.html), [job control probe](../../tests/attempts/pythonMachine/jobControlProbe.html), [pipe and shm probe](../../tests/attempts/pythonMachine/pipeShmProbe.html) | `npm run test:browser`, the MachineContainer and JobControl rows of `npm run test:consumer`, process OS probes | Not a Linux container and not an OS scheduler. It is pyproc's browser-kernel isolation model |
6060
| Guests on one wire | Two guests on one computer exchange real Ethernet frames instead of merely coexisting | The built-in `network` device of `createWebComputer` (a `MemoryEthernetSwitch`), the `pyprocNet` module inside the Python guest, and the frame law exported from `pyproc/machine` | Experimental | `createWebComputer` with `network` left on, and a guest that speaks frames | [guest network probe](../../tests/webMachine/browser/probes/guestNetworkProbe.html), [packet network probe](../../tests/webMachine/browser/probes/packetNetworkProbe.html) | `npm run test:web-machine` (the guest network probe runs in CI; the Linux crossing is in the local `--v86` lane) | Only ARP and ICMP echo are answered automatically; TCP, UDP, and DNS are the guest's own business. There is no uplink, so guests reach each other and nothing else. Both guests currently share one JS thread, so a frame arrives between `run()` calls rather than during one |
61-
| Non-Pyodide engine seam | Proves the session and time-travel primitives work on non-Pyodide CPython 3.14 WASI | `bootWasi`, `WasiSession` (`pyproc/wasi`) | Research preview | A consumer-provided `wasmURL`, the WASI worker asset, and the JSON value bridge | [WASI browser gate](../../tests/browser/wasiGate.html), [engine port campaign](../../tests/attempts/enginePort/README.md), [WASI REPL probe](../../tests/attempts/enginePort/wasiReplProbe.html) | WASI gate, [dual engine probe](../../tests/webMachine/browser/probes/dualEngineProbe.html) in `npm run test:web-machine` | Not the production default engine. The value bridge is JSON-centric and C extensions need a static build |
61+
| Non-Pyodide engine seam | Proves the session and time-travel primitives work on non-Pyodide CPython 3.14 WASI | `bootWasi`, `WasiSession` (`pyproc/wasi`) | Research preview | A consumer-provided `wasmURL`, the WASI worker asset, and the JSON value bridge | [WASI browser gate](../../tests/browser/wasiGate.html), [engine port campaign](../../tests/attempts/enginePort/README.md), [WASI REPL probe](../../tests/attempts/enginePort/wasiReplProbe.html) | WASI gate, [dual engine probe](../../tests/webMachine/browser/probes/dualEngineProbe.html) in `npm run test:web-machine:wasi` | Not the production default engine. The value bridge is JSON-centric and C extensions need a static build |
6262

6363
## Product decision rules
6464

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"mcp:sandbox": "node scripts/mcpSandboxServer.mjs",
8383
"test:preflight": "node tests/browser/run.mjs tests/browser/preflightNoCoi.html",
8484
"test:web-machine": "node tests/webMachine/run.mjs",
85+
"test:web-machine:wasi": "node scripts/fetchWasiAssets.mjs && node tests/webMachine/run.mjs --wasi",
8586
"test:web-machine:v86": "node tests/webMachine/fixtures/v86/prepareAssets.mjs && node tests/webMachine/run.mjs --v86",
8687
"test:socket": "node tests/browser/socketLane.mjs",
8788
"test:golden": "node tests/browser/goldenWorkflow.mjs"

tests/browser/wasiGate.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ <h1>pyproc WASI 게이트 - 승격된 WasiSession 실배선</h1>
1313
<script type="module">
1414
// pyproc/wasi subpath 표면(루트에서 강등됨). 게이트는 소스 경로로 같은 모듈을 연다.
1515
import { bootWasi } from "../../src/runtime/engines/wasi/wasiSession.js";
16+
import { assertRuntimeContract } from "../../src/runtime/runtimeContract.js";
1617
const out = document.getElementById("out");
1718
const checks = []; const timings = {};
1819
const check = (name, pass, info = "") => { checks.push({ name, pass: !!pass, info: String(info) }); out.textContent += `\n${pass ? "PASS" : "FAIL"} ${name}${info ? " (" + info + ")" : ""}`; };
@@ -31,6 +32,14 @@ <h1>pyproc WASI 게이트 - 승격된 WasiSession 실배선</h1>
3132
let t = performance.now();
3233
const s = await bootWasi({ wasmURL: WASM_URL, stdlibURL: STDLIB_URL });
3334
timings.bootMs = Math.round(performance.now() - t);
35+
check("WASI runtime 공통 계약", assertRuntimeContract(s) === s && s.runtimeKind === "wasi" && s.runtimeContractVersion === 1);
36+
const capabilities = s.capabilities();
37+
check("WASI capability 선언", ["asyncExecution", "globals", "hostValues", "checkpoint", "packages"].every((name) => capabilities.includes(name)));
38+
const asyncResult = await s.runAsync("print('async-ok')");
39+
check("RuntimeContract runAsync 별칭", asyncResult.trim() === "async-ok", asyncResult.trim());
40+
await s.setGlobal("aliasPayload", { branch: "wasi", n: 7 });
41+
const aliasPayload = await s.getGlobal("aliasPayload");
42+
check("RuntimeContract global 별칭", JSON.stringify(aliasPayload) === '{"branch":"wasi","n":7}', JSON.stringify(aliasPayload));
3443
await s.run("x = 41");
3544
await s.run("x = x + 1");
3645
const r1 = await s.run("import sys\nprint(x, sys.version.split()[0])");

tests/webMachine/run.mjs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import { fileURLToPath } from "node:url";
1515

1616
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
1717
const wantV86 = process.argv.includes("--v86");
18+
const wantWasi = wantV86 || process.argv.includes("--wasi");
1819

19-
// 자산 없이 도는 probe: host 계약, 3-guest 계약(WASI adapter 포함), owner 승계 경쟁.
20+
// 자산 없이 도는 probe: host 계약, owner 승계, Python guest networking, 내구성 계약.
2021
const ASSET_FREE = [
2122
"tests/webMachine/browser/probes/hostContractProbe.html",
22-
"tests/webMachine/browser/probes/dualEngineProbe.html",
2323
"tests/webMachine/browser/probes/ownerSuccessorProbe.html",
2424
"tests/webMachine/browser/probes/guestNetworkProbe.html",
2525
// 내구 커밋의 핵(찢어진 커밋·CAS 경쟁·세대 불변성·retention gc)을 보는 probe다. x86 자산이
@@ -35,6 +35,9 @@ const ASSET_FREE = [
3535
// memory store로 owner, restore-or-boot, save, signed import/export, dispose를 전부 문다.
3636
"tests/webMachine/browser/probes/durableComputerProbe.html",
3737
];
38+
const WASI_BACKED = [
39+
"tests/webMachine/browser/probes/dualEngineProbe.html",
40+
];
3841
// x86 자산이 필요한 probe. ownerSuccessorParticipant는 probe가 iframe으로 여는 참가자 페이지다.
3942
const V86_BACKED = [
4043
"tests/webMachine/browser/probes/dualBootProbe.html",
@@ -51,7 +54,12 @@ const V86_ASSET_DIR = join(ROOT, "tests", "webMachine", "fixtures", "v86", "asse
5154

5255
// --v86은 레인을 더한다(바꾸지 않는다). 치환이면 x86 레인을 돌 때 자산 없이 도는 계약
5356
// 3개가 빠져, 두 레인을 함께 돈 적이 한 번도 없게 된다(외부 감사 지적, 2026-07-27).
54-
const pages = wantV86 ? [...ASSET_FREE, ...V86_BACKED] : ASSET_FREE;
57+
const pages = [...ASSET_FREE, ...(wantWasi ? WASI_BACKED : []), ...(wantV86 ? V86_BACKED : [])];
58+
const WASI_ASSET_DIR = join(ROOT, "tests", "attempts", "enginePort");
59+
if (wantWasi && (!existsSync(join(WASI_ASSET_DIR, "python-3.14.6.wasm")) || !existsSync(join(WASI_ASSET_DIR, "python314-stdlib.zip")))) {
60+
console.error(`FAIL WASI 자산 없음: ${WASI_ASSET_DIR}. node scripts/fetchWasiAssets.mjs를 먼저 실행한다.`);
61+
process.exit(1);
62+
}
5563
if (wantV86 && !existsSync(join(V86_ASSET_DIR, "libv86.mjs"))) {
5664
console.error(`FAIL v86 자산 없음: ${V86_ASSET_DIR}. npm run test:web-machine:v86가 prepareAssets를 먼저 돈다.`);
5765
process.exit(1);

0 commit comments

Comments
 (0)