test(python/omp-rpc): reworked host URI tests to capture uri_echo notifications

- Adjusted the test server in test_host_uris.py to re-emit host_uri_result frames as uri_echo notifications.
- Updated test client setup to attach an unknown-notification capture hook for uri_echo frames.
- Changed _await_echo to read captured uri_echo frames from that hook and fail fast if capture is missing.
This commit is contained in:
can1357
2026-05-15 00:55:56 +02:00
parent 3e9ca3e279
commit 05117a0258
2 changed files with 169 additions and 13 deletions
@@ -0,0 +1,144 @@
import { afterEach, describe, expect, it } from "bun:test";
import { InternalUrlRouter } from "@oh-my-pi/pi-coding-agent/internal-urls";
import { parseInternalUrl } from "@oh-my-pi/pi-coding-agent/internal-urls/parse";
import { RpcHostUriBridge } from "@oh-my-pi/pi-coding-agent/modes/rpc/host-uris";
import type { RpcHostUriCancelRequest, RpcHostUriRequest } from "@oh-my-pi/pi-coding-agent/modes/rpc/rpc-types";
const router = InternalUrlRouter.instance();
afterEach(() => {
// Tests register transient schemes on the global router; clean them up
// between cases so neighboring suites observe the default registration.
router.unregister("db");
router.unregister("notes");
router.unregister("Db"); // case-insensitive guard
});
function recordOutput(): {
frames: Array<RpcHostUriRequest | RpcHostUriCancelRequest>;
push: (frame: RpcHostUriRequest | RpcHostUriCancelRequest) => void;
} {
const frames: Array<RpcHostUriRequest | RpcHostUriCancelRequest> = [];
return { frames, push: frame => frames.push(frame) };
}
describe("RpcHostUriBridge", () => {
it("registers schemes against the router and surfaces read results", async () => {
const out = recordOutput();
const bridge = new RpcHostUriBridge(out.push);
bridge.setSchemes([{ scheme: "db", description: "rows", writable: false }]);
expect(router.canHandle("db://users/42")).toBe(true);
const pending = bridge.requestRead("db", parseInternalUrl("db://users/42"));
expect(out.frames).toHaveLength(1);
const request = out.frames[0];
if (!request || request.type !== "host_uri_request") {
throw new Error("Expected host_uri_request frame");
}
expect(request.operation).toBe("read");
expect(request.url).toBe("db://users/42");
bridge.handleResult({
type: "host_uri_result",
id: request.id,
content: "id=42",
contentType: "application/json",
notes: ["fresh"],
});
const resource = await pending;
expect(resource.content).toBe("id=42");
expect(resource.contentType).toBe("application/json");
expect(resource.notes).toEqual(["fresh"]);
bridge.clear("test cleanup");
});
it("attaches a write hook only for writable schemes", async () => {
const out = recordOutput();
const bridge = new RpcHostUriBridge(out.push);
bridge.setSchemes([
{ scheme: "db", writable: true },
{ scheme: "notes", writable: false },
]);
const dbHandler = router.getHandler("db");
const notesHandler = router.getHandler("notes");
expect(typeof dbHandler?.write).toBe("function");
expect(notesHandler?.write).toBeUndefined();
const url = parseInternalUrl("db://users/42");
const pending = bridge.requestWrite("db", url, "new content");
expect(out.frames).toHaveLength(1);
const request = out.frames[0];
if (!request || request.type !== "host_uri_request") {
throw new Error("Expected host_uri_request frame");
}
expect(request.operation).toBe("write");
expect(request.content).toBe("new content");
bridge.handleResult({ type: "host_uri_result", id: request.id });
await expect(pending).resolves.toBeUndefined();
bridge.clear("test cleanup");
});
it("propagates host-reported errors as exceptions", async () => {
const out = recordOutput();
const bridge = new RpcHostUriBridge(out.push);
bridge.setSchemes([{ scheme: "db", writable: true }]);
const url = parseInternalUrl("db://users/42");
const pending = bridge.requestRead("db", url);
const request = out.frames[0];
if (!request || request.type !== "host_uri_request") {
throw new Error("Expected host_uri_request frame");
}
bridge.handleResult({
type: "host_uri_result",
id: request.id,
isError: true,
error: "row not found",
});
await expect(pending).rejects.toThrow("row not found");
bridge.clear("test cleanup");
});
it("emits a cancel frame when the read signal aborts", async () => {
const out = recordOutput();
const bridge = new RpcHostUriBridge(out.push);
bridge.setSchemes([{ scheme: "db" }]);
const controller = new AbortController();
const url = parseInternalUrl("db://users/42");
const pending = bridge.requestRead("db", url, { signal: controller.signal });
expect(out.frames).toHaveLength(1);
controller.abort();
await expect(pending).rejects.toThrow(/aborted/);
const cancel = out.frames[1];
expect(cancel?.type).toBe("host_uri_cancel");
bridge.clear("test cleanup");
});
it("normalizes scheme casing and rejects invalid characters", () => {
const bridge = new RpcHostUriBridge(() => {});
const accepted = bridge.setSchemes([{ scheme: " DB " }]);
expect(accepted).toEqual(["db"]);
expect(router.canHandle("db://x")).toBe(true);
expect(() => bridge.setSchemes([{ scheme: "1bad" }])).toThrow();
bridge.clear("test cleanup");
});
it("replaces the registered set and unregisters schemes that drop off", () => {
const bridge = new RpcHostUriBridge(() => {});
bridge.setSchemes([{ scheme: "db" }, { scheme: "notes" }]);
expect(router.canHandle("notes://idx")).toBe(true);
bridge.setSchemes([{ scheme: "db" }]);
expect(router.canHandle("notes://idx")).toBe(false);
expect(router.canHandle("db://idx")).toBe(true);
bridge.clear("test cleanup");
});
});
+25 -13
View File
@@ -73,14 +73,13 @@ URI_SERVER = textwrap.dedent(
pending_uri_id += 1
respond(request_id, "trigger_write", {})
elif command_type == "host_uri_result":
# Echo back as response so the test can assert on the wire frame
# Re-emit the host_uri_result frame as an unknown notification
# so the test can capture it through on_unknown_notification.
print(
json.dumps(
{
"type": "response",
"command": "uri_echo",
"success": True,
"data": {"frame": command},
"type": "uri_echo",
"frame": command,
}
),
flush=True,
@@ -132,12 +131,14 @@ class HostUriHelperTests(unittest.TestCase):
class RpcHostUriBridgeTests(unittest.TestCase):
def _make_client(self, **kwargs: object) -> RpcClient:
return RpcClient(
client = RpcClient(
command=[sys.executable, "-u", "-c", URI_SERVER],
startup_timeout=2.0,
request_timeout=2.0,
**kwargs,
)
self._attach_capture(client)
return client
def test_set_host_uris_registers_schemes_on_start(self) -> None:
captured: list[tuple[str, str]] = []
@@ -218,18 +219,29 @@ class RpcHostUriBridgeTests(unittest.TestCase):
self.assertEqual(frame["error"], "boom")
def _await_echo(self, client: RpcClient) -> dict:
# The fake server echoes the host_uri_result frame back as an
# `uri_echo` response. We poll the events history to surface it.
captured = getattr(client, "_test_uri_echos", None)
if captured is None:
self.fail("_capture was not called for this client")
deadline = time.time() + 2.0
while time.time() < deadline:
with client._state_lock: # type: ignore[attr-defined]
events = client._events.snapshot() # type: ignore[attr-defined]
for event in events:
if event.get("command") == "uri_echo" and event.get("data"):
return event["data"]["frame"]
if captured:
return captured.pop(0)
time.sleep(0.02)
self.fail("Timed out waiting for host_uri_result echo")
def _attach_capture(self, client: RpcClient) -> None:
captured: list[dict] = []
client._test_uri_echos = captured # type: ignore[attr-defined]
def on_notification(notification) -> None:
payload = notification.payload
if payload.get("type") == "uri_echo":
frame = payload.get("frame")
if isinstance(frame, dict):
captured.append(frame)
client.on_unknown_notification(on_notification)
if __name__ == "__main__":
unittest.main()