Skip to content

Commit 0b33972

Browse files
committed
test: add e2e test proving Python<->Node JSON-RPC protocol works
- Python spawns Node plugin subprocess (same as flyto-core runtime) - Tests handshake, invoke (echo/add), context passing, error handling, ping, shutdown - 9 tests, all passing
1 parent 91e1025 commit 0b33972

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Minimal test plugin for e2e testing with flyto-core
2+
import { createPlugin } from "./index.js";
3+
4+
const plugin = createPlugin({ id: "test/echo", version: "0.1.0" });
5+
6+
plugin.step("echo", async (input) => ({
7+
ok: true,
8+
data: { echo: input.message, reversed: String(input.message).split("").reverse().join("") },
9+
}));
10+
11+
plugin.step("add", async (input) => ({
12+
ok: true,
13+
data: { result: Number(input.a) + Number(input.b) },
14+
}));
15+
16+
plugin.step("fail", async () => {
17+
throw new Error("intentional test failure");
18+
});
19+
20+
plugin.start();

tests/test_e2e_core.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
"""
3+
End-to-end test: flyto-core Python ↔ Node.js plugin via JSON-RPC stdio.
4+
5+
Spawns the echo test plugin as a subprocess (same as flyto-core runtime would),
6+
sends handshake + invoke + ping + shutdown, verifies responses.
7+
"""
8+
9+
import asyncio
10+
import json
11+
import os
12+
import sys
13+
from pathlib import Path
14+
15+
PLUGIN_JS = Path(__file__).parent.parent / "packages" / "sdk" / "dist" / "echo-test-plugin.js"
16+
17+
async def send_recv(proc, method, params=None, msg_id=1):
18+
"""Send a JSON-RPC request and read the response."""
19+
request = {"jsonrpc": "2.0", "method": method, "id": msg_id}
20+
if params:
21+
request["params"] = params
22+
23+
line = json.dumps(request) + "\n"
24+
proc.stdin.write(line.encode())
25+
await proc.stdin.drain()
26+
27+
raw = await asyncio.wait_for(proc.stdout.readline(), timeout=5.0)
28+
return json.loads(raw.decode().strip())
29+
30+
31+
async def run_tests():
32+
assert PLUGIN_JS.exists(), f"Build first: {PLUGIN_JS}"
33+
34+
# Spawn plugin — same as flyto-core runtime/process.py does
35+
proc = await asyncio.create_subprocess_exec(
36+
"node", str(PLUGIN_JS),
37+
stdin=asyncio.subprocess.PIPE,
38+
stdout=asyncio.subprocess.PIPE,
39+
stderr=asyncio.subprocess.PIPE,
40+
)
41+
42+
passed = 0
43+
failed = 0
44+
45+
try:
46+
# ── Test 1: Handshake ────────────────────────────
47+
res = await send_recv(proc, "handshake", {
48+
"protocolVersion": "0.1.0",
49+
"pluginId": "test/echo",
50+
"executionId": "e2e-test",
51+
}, msg_id=1)
52+
53+
assert res["id"] == 1, f"Wrong id: {res}"
54+
result = res["result"]
55+
assert result["pluginVersion"] == "0.1.0", f"Wrong version: {result}"
56+
assert "echo" in result["steps"], f"Missing echo step: {result}"
57+
assert "add" in result["steps"], f"Missing add step: {result}"
58+
print(" PASS: handshake")
59+
passed += 1
60+
61+
# ── Test 2: Invoke echo ──────────────────────────
62+
res = await send_recv(proc, "invoke", {
63+
"step": "echo",
64+
"input": {"message": "hello flyto"},
65+
}, msg_id=2)
66+
67+
result = res["result"]
68+
assert result["ok"] is True, f"Not ok: {result}"
69+
assert result["data"]["echo"] == "hello flyto", f"Wrong echo: {result}"
70+
assert result["data"]["reversed"] == "otylf olleh", f"Wrong reverse: {result}"
71+
print(" PASS: invoke echo")
72+
passed += 1
73+
74+
# ── Test 3: Invoke add ───────────────────────────
75+
res = await send_recv(proc, "invoke", {
76+
"step": "add",
77+
"input": {"a": 17, "b": 25},
78+
}, msg_id=3)
79+
80+
result = res["result"]
81+
assert result["ok"] is True, f"Not ok: {result}"
82+
assert result["data"]["result"] == 42, f"Wrong sum: {result}"
83+
print(" PASS: invoke add")
84+
passed += 1
85+
86+
# ── Test 4: Invoke with context ──────────────────
87+
res = await send_recv(proc, "invoke", {
88+
"step": "echo",
89+
"input": {"message": "ctx-test"},
90+
"context": {
91+
"execution_id": "exec-999",
92+
"browser_ws_endpoint": "ws://localhost:9222",
93+
},
94+
}, msg_id=4)
95+
96+
result = res["result"]
97+
assert result["ok"] is True, f"Not ok: {result}"
98+
assert result["data"]["echo"] == "ctx-test", f"Wrong echo: {result}"
99+
print(" PASS: invoke with context")
100+
passed += 1
101+
102+
# ── Test 5: Invoke unknown step ──────────────────
103+
res = await send_recv(proc, "invoke", {
104+
"step": "nonexistent",
105+
"input": {},
106+
}, msg_id=5)
107+
108+
result = res["result"]
109+
assert result["ok"] is False, f"Should fail: {result}"
110+
assert result["error"]["code"] == "STEP_NOT_FOUND", f"Wrong error: {result}"
111+
print(" PASS: invoke unknown step returns error")
112+
passed += 1
113+
114+
# ── Test 6: Invoke step that throws ──────────────
115+
res = await send_recv(proc, "invoke", {
116+
"step": "fail",
117+
"input": {},
118+
}, msg_id=6)
119+
120+
result = res["result"]
121+
assert result["ok"] is False, f"Should fail: {result}"
122+
assert "intentional" in result["error"]["message"], f"Wrong error msg: {result}"
123+
print(" PASS: invoke throwing step returns error")
124+
passed += 1
125+
126+
# ── Test 7: Ping ─────────────────────────────────
127+
res = await send_recv(proc, "ping", msg_id=7)
128+
129+
result = res["result"]
130+
assert result["status"] == "ok", f"Ping failed: {result}"
131+
print(" PASS: ping")
132+
passed += 1
133+
134+
# ── Test 8: Shutdown ─────────────────────────────
135+
res = await send_recv(proc, "shutdown", msg_id=8)
136+
137+
result = res["result"]
138+
assert result["status"] == "shutdown", f"Shutdown failed: {result}"
139+
print(" PASS: shutdown")
140+
passed += 1
141+
142+
# Wait for process to exit
143+
await asyncio.wait_for(proc.wait(), timeout=3.0)
144+
assert proc.returncode == 0, f"Non-zero exit: {proc.returncode}"
145+
print(" PASS: clean exit")
146+
passed += 1
147+
148+
except Exception as e:
149+
print(f" FAIL: {e}")
150+
failed += 1
151+
# Kill if still running
152+
if proc.returncode is None:
153+
proc.kill()
154+
await proc.wait()
155+
156+
print(f"\n{'='*50}")
157+
print(f"E2E Results: {passed} passed, {failed} failed")
158+
print(f"{'='*50}")
159+
160+
return 1 if failed > 0 else 0
161+
162+
163+
if __name__ == "__main__":
164+
print("E2E Test: Python (flyto-core) <-> Node.js (@flyto/plugin-sdk)")
165+
print("="*50)
166+
exit_code = asyncio.run(run_tests())
167+
sys.exit(exit_code)

0 commit comments

Comments
 (0)