fix: harden workflow console rpc boundary

This commit is contained in:
lda
2026-07-02 14:28:25 +07:00 Verified
parent f112f73a0e
commit bcf31583af
20 changed files with 890 additions and 465 deletions
+39 -109
View File
@@ -1,127 +1,57 @@
import { Effect, Either } from "effect";
import { describe, expect, it } from "vitest";
import { Effect, Ref } from "effect";
import { RpcClient, RpcGroup, RpcSerialization } from "@effect/rpc";
import { FetchHttpClient, HttpClient } from "@effect/platform";
import { WorkflowHealth, WorkflowSourcesList } from "./rpcs.js";
import { withEvidenceCapture, type EvidenceRecord } from "./evidence.js";
import { WorkflowRpc, makeWorkflowRpcLayer } from "./service.js";
const LIVE = process.env.LIVE_PYTHON_SERVER === "1";
const describeLive = LIVE ? describe : describe.skip;
const TARGET = "http://127.0.0.1:8765/rpc";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const run = <A>(eff: Effect.Effect<A, any, any>): Promise<A> =>
Effect.runPromise(eff as Effect.Effect<A, never, never>);
const runOperation = (operation: string, params: unknown = {}) =>
Effect.gen(function* () {
const rpc = yield* WorkflowRpc;
return yield* rpc.execute(operation, TARGET, params);
}).pipe(Effect.provide(makeWorkflowRpcLayer()), Effect.runPromise);
describeLive("interop: live Python server", () => {
it("workflow.health returns ok", async () => {
const group = RpcGroup.make(WorkflowHealth);
const result = await run(
Effect.gen(function* () {
const client = yield* RpcClient.make(group as any).pipe(
Effect.provide(RpcSerialization.layerJsonRpc()),
Effect.provide(FetchHttpClient.layer),
Effect.provide(
RpcClient.layerProtocolHttp({ url: TARGET } as any),
),
);
return yield* (client as any)["workflow.health"]({});
}).pipe(
Effect.scoped,
Effect.provide(FetchHttpClient.layer),
Effect.provide(RpcSerialization.layerJsonRpc()),
),
);
expect(result).toEqual({
it("workflow.health returns interpreted status and raw JSON-RPC evidence", async () => {
const exchange = await runOperation("workflow.health");
expect(exchange.interpreted).toMatchObject({
status: "ok",
store_root: expect.any(String),
storeRoot: expect.any(String),
});
expect(exchange.exchange.request).toMatchObject({
jsonrpc: "2.0",
method: "workflow.health",
});
expect(exchange.exchange.response).toMatchObject({
jsonrpc: "2.0",
result: { status: "ok" },
});
});
it("workflow.sources.list returns paginated results", async () => {
const group = RpcGroup.make(WorkflowSourcesList);
const result = await run(
Effect.gen(function* () {
const client = yield* RpcClient.make(group as any).pipe(
Effect.provide(RpcSerialization.layerJsonRpc()),
Effect.provide(FetchHttpClient.layer),
Effect.provide(
RpcClient.layerProtocolHttp({ url: TARGET } as any),
),
);
return yield* (client as any)["workflow.sources.list"]({
limit: 10,
});
}).pipe(
Effect.scoped,
Effect.provide(FetchHttpClient.layer),
Effect.provide(RpcSerialization.layerJsonRpc()),
),
);
const typed = result as {
sources: unknown[];
next_cursor: string | null;
total: number;
};
expect(typed.sources).toBeInstanceOf(Array);
expect(typeof typed.total).toBe("number");
it("workflow.sources.list returns paginated interpreted results", async () => {
const exchange = await runOperation("workflow.sources.list", { limit: 10 });
expect(exchange.interpreted).toMatchObject({
sources: expect.any(Array),
total: expect.any(Number),
});
expect(exchange.equivalentCli).toContain("uv run wf source list");
});
it("handles standard JSON-RPC errors gracefully", async () => {
const group = RpcGroup.make(WorkflowSourcesList);
const exit = await Effect.runPromiseExit(
Effect.gen(function* () {
const client = yield* RpcClient.make(group as any).pipe(
Effect.provide(RpcSerialization.layerJsonRpc()),
Effect.provide(FetchHttpClient.layer),
Effect.provide(
RpcClient.layerProtocolHttp({ url: TARGET } as any),
),
);
return yield* (client as any)["workflow.sources.list"]({
cursor: "nonexistent",
});
}).pipe(
Effect.scoped,
Effect.provide(FetchHttpClient.layer),
Effect.provide(RpcSerialization.layerJsonRpc()),
) as Effect.Effect<unknown, never, never>,
);
expect(exit._tag).toBe("Success");
});
it("unknown operations fail before reaching the server", async () => {
const result = await Effect.gen(function* () {
const rpc = yield* WorkflowRpc;
return yield* rpc
.execute("workflow.nope", TARGET, {})
.pipe(Effect.either);
}).pipe(Effect.provide(makeWorkflowRpcLayer()), Effect.runPromise);
it("raw evidence capture works", async () => {
const evidenceRef = Effect.runSync(Ref.make<EvidenceRecord | null>(null));
const group = RpcGroup.make(WorkflowHealth);
const result = await run(
Effect.gen(function* () {
const httpClient = yield* HttpClient.HttpClient;
const transformed = withEvidenceCapture(httpClient, evidenceRef);
const protocolLayer = RpcClient.layerProtocolHttp({
url: TARGET,
transformClient: () => transformed,
} as any);
const client = yield* RpcClient.make(group as any).pipe(
Effect.provide(RpcSerialization.layerJsonRpc()),
Effect.provide(FetchHttpClient.layer),
Effect.provide(protocolLayer),
);
return yield* (client as any)["workflow.health"]({});
}).pipe(
Effect.scoped,
Effect.provide(FetchHttpClient.layer),
Effect.provide(RpcSerialization.layerJsonRpc()),
),
);
const evidence = Effect.runSync(Ref.get(evidenceRef));
expect(evidence).not.toBeNull();
expect(evidence!.request.url).toContain("/rpc");
expect(evidence!.response.status).toBe(200);
expect(Either.isLeft(result)).toBe(true);
if (Either.isLeft(result)) {
expect(result.left._tag).toBe("UnknownOperationError");
}
});
});