import { Effect, Either } from "effect"; import { describe, expect, it } from "vitest"; import { RpcDecodeError, RpcProtocolError, RpcRemoteError, UpstreamConnectionError, UpstreamResponseTooLargeError, UpstreamTimeoutError, } from "./errors.js"; import { WorkflowRpc, makeWorkflowRpcLayer, type OperationExchange, type WorkflowRpcOptions, } from "./service.js"; import type { OperationName } from "./method-registry.js"; type JsonRpcRequest = { readonly jsonrpc: "2.0"; readonly id: number | string; readonly method: string; readonly params: unknown; }; const bodyText = async ( body: RequestInit["body"] | null | undefined, ): Promise => { if (typeof body === "string") return body; if (body instanceof Uint8Array) return new TextDecoder().decode(body); if (body instanceof Blob) return body.text(); if (body instanceof ReadableStream) return new Response(body).text(); throw new Error("expected JSON-RPC request body"); }; const requestBody = async ( input: Parameters[0], init?: RequestInit, ): Promise => { if (input instanceof Request) { return JSON.parse(await input.clone().text()) as JsonRpcRequest; } return JSON.parse(await bodyText(init?.body ?? null)) as JsonRpcRequest; }; const jsonResponse = (body: unknown, status = 200): Response => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" }, }); const runOperation = ( options: WorkflowRpcOptions, operation: OperationName = "workflow.health", params: unknown = {}, ): Promise => Effect.gen(function* () { const rpc = yield* WorkflowRpc; return yield* rpc.execute( operation, "http://127.0.0.1:8765/rpc", params, ); }).pipe(Effect.provide(makeWorkflowRpcLayer(options)), Effect.runPromise); const runEither = ( options: WorkflowRpcOptions, ): Promise> => Effect.gen(function* () { const rpc = yield* WorkflowRpc; return yield* rpc .execute("workflow.health", "http://127.0.0.1:8765/rpc", {}) .pipe(Effect.either); }).pipe(Effect.provide(makeWorkflowRpcLayer(options)), Effect.runPromise); const draftWorkspaceResult = { workspace_id: "console.demo", revision: 2, title: "Console demo", status: "valid" as const, diagnostics: [], summary: { name: "console.demo", start: "echo", step_count: 1, route_count: 1, steps: ["echo"], }, draft: { name: "console.demo", start: "echo", steps: { echo: { use: "local.example.echo" } }, routes: { echo: { ok: "__end__" } }, }, }; const createFromCapabilityResult = { ...draftWorkspaceResult, next_actions: { can_continue: false, can_save_now: true, recommended_next_tool: "workflow.draft_workspaces.validate", reason: "draft is ready for validation", patch_examples: [], warnings: [], }, wrapper_hints: { capability_name: "local.example.echo", confidence: "high" as const, declared_outcomes: ["ok"], input_map: {}, input_schema: { type: "object" }, missing_decisions: [], notes: [], outcome_candidates: [], outcome_policy: "preserve_declared" as const, output_map: {}, output_schema: { type: "object" }, state_schema: { type: "object" }, suggested_wrapper_outcomes: ["ok"], }, }; const lifecycleCases = [ { operation: "workflow.capabilities.list" as const, params: { query: "document", source_id: "local.lda_docs", limit: 25 }, result: { capabilities: [ { kind: "node_spec", name: "local.lda_docs.read_documents", source_id: "local.lda_docs", description: "Read selected project documents.", outcomes: ["ok", "error"], is_async: false, input_fields: ["names"], output_fields: ["documents"], }, ], next_cursor: null, total: 1, }, }, { operation: "workflow.capabilities.inspect" as const, params: { qualified_name: "local.lda_docs.read_documents" }, result: { kind: "node_spec", name: "local.lda_docs.read_documents", source_id: "local.lda_docs", description: "Read selected project documents.", is_async: false, accepts_context: false, outcomes: ["ok", "error"], input_schema: { type: "object", properties: { names: { type: "array" } } }, output_schema: { type: "object", properties: { documents: { type: "array" } } }, wrapper_hints: { capability_name: "local.lda_docs.read_documents", confidence: "high", declared_outcomes: ["ok", "error"], input_map: { names: "input.names" }, input_schema: { type: "object" }, missing_decisions: [], notes: [], outcome_candidates: [], outcome_policy: "preserve_declared", output_map: { documents: "output.documents" }, output_schema: { type: "object" }, state_schema: { type: "object" }, suggested_wrapper_outcomes: ["ok", "error"], }, }, }, { operation: "workflow.draft_workspaces.list" as const, params: {}, result: { workspaces: [ { workspace_id: "console.demo", revision: 1, title: "Console demo", status: "valid", diagnostics: [], summary: { name: "console.demo", start: "read", step_count: 1, route_count: 1, steps: ["read"], }, }, ], }, }, { operation: "workflow.draft_workspaces.get" as const, params: { workspace_id: "console.demo", include_draft: true }, result: { workspace_id: "console.demo", revision: 1, title: "Console demo", status: "valid", diagnostics: [], summary: { name: "console.demo", start: "read", step_count: 1, route_count: 1, steps: ["read"], }, draft: { name: "console.demo", start: "read", steps: { read: { use: "local.lda_docs.read_documents" } }, routes: { read: { ok: "__end__" } }, }, }, }, { operation: "workflow.draft_workspaces.inspect_authoring_contract" as const, params: { workspace_id: "console.demo", revision: 1, selected_step_id: "echo", }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.create_empty" as const, params: { workspace_id: "console.demo", name: "console.demo", title: "Console demo", outcomes: ["ok"], }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.create_from_capability" as const, params: { workspace_id: "console.demo", capability_name: "local.example.echo", name: "console.demo", input_map: { text: "input.text" }, output_map: { text: "state.text" }, }, result: createFromCapabilityResult, }, { operation: "workflow.draft_workspaces.add_step_from_capability" as const, params: { workspace_id: "console.demo", revision: 1, step_id: "echo", capability_name: "local.example.echo", route_from_step: null, route_from_outcome: "ok", routes: { ok: "__end__" }, input_map: { text: "input.text" }, bind_outputs: { text: "state.text" }, retry: 1, timeout_seconds: 30, }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.update_capability_step" as const, params: { workspace_id: "console.demo", revision: 2, step_id: "echo", update: { desc: "Echo text", retry: 2, timeout_seconds: 45 }, }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.set_route" as const, params: { workspace_id: "console.demo", revision: 2, step_id: "echo", outcome: "ok", target: "__end__", }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.set_contract" as const, params: { workspace_id: "console.demo", revision: 2, outcomes: ["ok", "error"], }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.set_start" as const, params: { workspace_id: "console.demo", revision: 3, step_id: "echo", }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.set_step_input_bindings" as const, params: { workspace_id: "console.demo", revision: 3, step_id: "render", bindings: [ { path: "input.title", target: "report.title" }, { target: "format", value: "markdown" }, ], }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.set_step_output_bindings" as const, params: { workspace_id: "console.demo", revision: 4, step_id: "render", bindings: [{ source: "report", target: "state.report" }], }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.validate" as const, params: { workspace_id: "console.demo" }, result: draftWorkspaceResult, }, { operation: "workflow.draft_workspaces.set_workflow_output_bindings" as const, params: { workspace_id: "console.demo", revision: 4, bindings: [ { path: "state.report", target: "report" }, { target: "format", value: "markdown" }, ], }, result: draftWorkspaceResult, }, { operation: "workflow.artifacts.list" as const, params: { limit: 50 }, result: { nodes: [ { name: "workflow.report@1", artifact_id: "report", version: 1, kind: "workflow", display_name: "Report", description: null, outcomes: ["ok"], input_schema: { type: "object" }, output_schema: { type: "object" }, required_sources: ["local.report"], diagnostics: [], }, ], total: 1, next_cursor: null, limit: 50, }, }, { operation: "workflow.artifacts.inspect" as const, params: { artifact_id: "report", version: 1 }, result: { id: "report", version: 1, title: "Report", kind: "workflow", description: null, outcomes: ["ok"], input_schema: { type: "object" }, output_schema: { type: "object" }, plan: { nodes: [], edges: [] }, required_capabilities: [], workflow_dependencies: {}, created_from_catalog_version: null, }, }, { operation: "workflow.deployments.list" as const, params: {}, result: { deployments: [ { id: "report.default", artifact_id: "report", artifact_version: 1, binding_count: 1, drift_policy: "block", }, ], }, }, { operation: "workflow.deployments.inspect" as const, params: { deployment_id: "report.default" }, result: { id: "report.default", artifact_id: "report", artifact_version: 1, bindings: [{ logical_source: "local.report", concrete_source: "report" }], drift_policy: "block", }, }, { operation: "workflow.deployments.validate" as const, params: { deployment_id: "report.default" }, result: { deployment_id: "report.default", artifact_id: "report", artifact_version: 1, status: "runnable", diagnostics: [], next_actions: { can_continue: true, can_save_now: null, recommended_next_tool: null, reason: "deployment is valid", patch_examples: [], warnings: [], }, }, }, { operation: "workflow.runs.list" as const, params: { limit: 50 }, result: { runs: [ { run_id: "run_1", deployment_id: "report.default", artifact_id: "report", artifact_version: 1, status: "interrupted", resume_readiness: "ready", diagnostic_count: 0, created_at: "2026-07-02T00:00:00Z", updated_at: "2026-07-02T00:00:01Z", }, ], total: 1, cursor: null, next_cursor: null, limit: 50, }, }, { operation: "workflow.runs.inspect" as const, params: { run_id: "run_1" }, result: { run_id: "run_1", deployment_id: "report.default", artifact_id: "report", artifact_version: 1, status: "interrupted", resume_readiness: "ready", interrupt: { id: "interrupt_1", frame_id: "frame_1", node_id: "review", kind: "review", payload: {}, resumable: true, route: null, outcomes: [], request_schema: { type: "object" }, resume_schema: { type: "object" }, typed: true, }, outcome: null, error: null, output: null, diagnostics: [], trace_count: 0, next_actions: { can_continue: false, can_save_now: null, recommended_next_tool: null, reason: "run is interrupted", patch_examples: [], warnings: [], }, }, }, { operation: "workflow.runs.start" as const, params: { deployment_id: "lda_report_case_study.default", workflow_input: { selected_documents: [ "project-brief.md", "architecture-notes.md", "evaluation-findings.md", "risk-register.md", "roadmap.md", ], board_path: "issue-board.json", }, trace_range: { start: 0, limit: 50 }, }, result: { run_id: "run_demo", deployment_id: "lda_report_case_study.default", artifact_id: "lda_report_case_study", artifact_version: 1, status: "interrupted", resume_readiness: "ready", interrupt: { id: "interrupt_demo", frame_id: "frame_review", node_id: "review_issues", kind: "issue_review", payload: { report_markdown: "# lda.chat Thesis And Project Readiness Report", proposed_issues: [ { id: "demo-issue-1", title: "Prepare demo script", body: "Write the defense walkthrough.", severity: "medium", }, ], }, resumable: true, route: null, outcomes: ["submitted", "cancelled"], request_schema: { type: "object", required: ["report_markdown", "proposed_issues"], }, resume_schema: { type: "object", required: ["approved", "selected_issue_ids"], }, typed: true, }, outcome: null, error: null, output: null, diagnostics: [], trace_count: 1, next_actions: { can_continue: true, can_save_now: null, recommended_next_tool: "wf.workflow.resume_run", reason: "run is interrupted", patch_examples: [], warnings: [], }, }, }, { operation: "workflow.runs.resume" as const, params: { run_id: "run_demo", resume_payload: { approved: true, selected_issue_ids: ["demo-issue-1"], comment: "Create selected issues.", }, resume_outcome: "submitted", trace_range: { start: 0, limit: 50 }, }, result: { run_id: "run_demo", deployment_id: "lda_report_case_study.default", artifact_id: "lda_report_case_study", artifact_version: 1, status: "completed", resume_readiness: "not_applicable", interrupt: null, outcome: "completed", error: null, output: { approved: true, markdown: "# lda.chat Thesis And Project Readiness Report", created_issues: [ { id: "ISSUE-001", title: "Prepare demo script", url: "local://issues/ISSUE-001", }, ], selected_issue_ids: ["demo-issue-1"], comment: "Create selected issues.", }, diagnostics: [], trace_count: 4, next_actions: { can_continue: false, can_save_now: null, recommended_next_tool: null, reason: "Run completed.", patch_examples: [], warnings: [], }, }, }, { operation: "workflow.runs.trace" as const, params: { run_id: "run_1", trace_range: { start: 0, limit: 50 } }, result: { run_id: "run_1", deployment_id: "report.default", artifact_id: "report", artifact_version: 1, status: "interrupted", resume_readiness: "ready", interrupt: { id: "interrupt_1", frame_id: "frame_1", node_id: "review", kind: "review", payload: {}, resumable: true, route: null, outcomes: ["submitted", "cancelled"], request_schema: { type: "object" }, resume_schema: { type: "object" }, typed: true, }, outcome: null, error: null, output: null, diagnostics: [], trace_count: 1, next_actions: { can_continue: true, can_save_now: null, recommended_next_tool: "wf.workflow.resume_run", reason: "run is interrupted", patch_examples: [], warnings: [], }, trace_start: 0, trace_limit: 50, trace_truncated: false, trace: [ { frame_id: "frame_1", node_id: "review", step_type: "interrupt", resolved_input: { report: "..." }, outcome: "submitted", output: {}, state_changes: {}, next_node_id: "create_issues", }, ], }, }, ] as const; describe("WorkflowRpc", () => { it("uses @effect/rpc and returns exact raw request and response evidence", async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: { status: "ok", store_root: "C:/store" }, }); }; const exchange = await runOperation({ fetch }); expect(exchange.target).toBe("http://127.0.0.1:8765/rpc"); expect(exchange.interpreted).toEqual({ status: "ok", storeRoot: "C:/store", }); expect(exchange.exchange.request).toMatchObject({ jsonrpc: "2.0", method: "workflow.health", params: {}, }); expect(exchange.exchange.response).toMatchObject({ jsonrpc: "2.0", result: { status: "ok", store_root: "C:/store" }, }); }); it("requests manual redirect handling", async () => { let redirect: RequestInit["redirect"]; const fetch: typeof globalThis.fetch = async (input, init) => { redirect = init?.redirect ?? (input instanceof Request ? input.redirect : undefined); const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: { status: "ok", store_root: "C:/store" }, }); }; await runOperation({ fetch }); expect(redirect).toBe("manual"); }); it("maps a standard foreign JSON-RPC error and preserves evidence", async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, error: { code: -32602, message: "Invalid params", data: { field: "x" } }, }); }; const result = await runEither({ fetch }); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(RpcRemoteError); expect((result.left as RpcRemoteError).exchange?.response).toMatchObject({ error: { code: -32602, message: "Invalid params" }, }); }); it("maps malformed successful results to decode errors with evidence", async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: { status: "wrong", store_root: "C:/store" }, }); }; const result = await runEither({ fetch }); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(RpcDecodeError); expect((result.left as RpcDecodeError).exchange?.response).toMatchObject({ result: { status: "wrong", store_root: "C:/store" }, }); }); it("decodes the canonical expanded source list result", async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: { sources: [ { id: "local.demo", kind: "python", enabled: true, visibility: { planner: true, client: true, admin_dashboard: true, }, permissions: { safe_for_workflow: true, calls_upstream: false, mutates_config: false, mutates_auth: false, }, policy: { platform: false, binding_required: true }, description: "Local demo source", tool_count: 1, node_spec_count: 0, reducer_count: 0, prompt_count: 0, resource_count: 0, preview: { tools: ["generate"], node_specs: [], reducers: [], prompts: [], resources: [], }, has_more: { tools: false, node_specs: false, reducers: false, prompts: false, resources: false, }, }, ], next_cursor: null, total: 1, }, }); }; const exchange = await runOperation( { fetch }, "workflow.sources.list", { limit: 50 }, ); expect(exchange.interpreted).toMatchObject({ total: 1, sources: [{ id: "local.demo", counts: { tools: 1 } }], }); }); it("rejects invalid source count shapes as decode errors", async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: { sources: [ { id: "local.demo", kind: "python", enabled: true, description: null, tool_count: -1, node_spec_count: 0, reducer_count: 0, prompt_count: 0, resource_count: 0, }, ], next_cursor: null, total: 1, }, }); }; const result = await Effect.gen(function* () { const rpc = yield* WorkflowRpc; return yield* rpc .execute( "workflow.sources.list", "http://127.0.0.1:8765/rpc", {}, ) .pipe(Effect.either); }).pipe(Effect.provide(makeWorkflowRpcLayer({ fetch })), Effect.runPromise); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(RpcDecodeError); }); it("fails with a bounded timeout", async () => { const fetch: typeof globalThis.fetch = () => new Promise(() => {}); const result = await runEither({ fetch, timeoutMilliseconds: 5 }); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(UpstreamTimeoutError); }); it("maps transport failures to upstream connection errors", async () => { const fetch: typeof globalThis.fetch = async () => { throw new Error("connection refused"); }; const result = await runEither({ fetch }); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(UpstreamConnectionError); }); it("rejects a response larger than the configured byte limit", async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: { status: "ok", store_root: "x".repeat(512) }, }); }; const result = await runEither({ fetch, maxResponseBytes: 128 }); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(UpstreamResponseTooLargeError); }); it("rejects a redirect response instead of decoding it", async () => { const fetch: typeof globalThis.fetch = async () => new Response("", { status: 302, headers: { location: "/elsewhere" } }); const result = await runEither({ fetch }); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(RpcProtocolError); }); }); describe("lifecycle operations", () => { for (const testCase of lifecycleCases) { it(`handles ${testCase.operation} successfully`, async () => { const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); expect(request.method).toBe(testCase.operation); expect(request.params).toEqual(testCase.params); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: testCase.result, }); }; const exchange = await runOperation( { fetch }, testCase.operation, testCase.params, ); expect(exchange.operation).toBe(testCase.operation); expect(exchange.interpreted).toBeDefined(); }); } it("interprets run trace frames with the console lifecycle shape", async () => { const traceCase = lifecycleCases.find( (testCase) => testCase.operation === "workflow.runs.trace", ); expect(traceCase).toBeDefined(); if (!traceCase) return; const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: traceCase.result, }); }; const exchange = await runOperation( { fetch }, traceCase.operation, traceCase.params, ); expect(exchange.interpreted).toMatchObject({ frames: [ { frameId: "frame_1", nodeId: "review", stepType: "interrupt", outcome: "submitted", nextNodeId: "create_issues", }, ], traceStart: 0, traceLimit: 50, traceTruncated: false, }); expect(exchange.interpreted).not.toHaveProperty("trace"); }); it("omits absent optional trace-range flags from the equivalent CLI", async () => { const traceCase = lifecycleCases.find( (testCase) => testCase.operation === "workflow.runs.trace", ); expect(traceCase).toBeDefined(); if (!traceCase) return; const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: traceCase.result, }); }; const exchange = await runOperation( { fetch }, "workflow.runs.trace", { run_id: "run_1", trace_range: {} }, ); expect(exchange.equivalentCli).toBe("uv run wf run trace run_1"); }); it("interprets typed interrupt contracts from run start", async () => { const startCase = lifecycleCases.find( (testCase) => testCase.operation === "workflow.runs.start", ); expect(startCase).toBeDefined(); if (!startCase) return; const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); return jsonResponse({ jsonrpc: "2.0", id: request.id, result: startCase.result, }); }; const exchange = await runOperation( { fetch }, startCase.operation, startCase.params, ); expect(exchange.interpreted).toMatchObject({ runId: "run_demo", status: "interrupted", interrupt: { kind: "issue_review", typed: true, outcomes: ["submitted", "cancelled"], }, nextActions: { canContinue: true, }, }); }); }); describe("capability call", () => { it("rejects malformed payloads before fetching", async () => { let fetchCalled = false; const fetch: typeof globalThis.fetch = async () => { fetchCalled = true; throw new Error("fetch must not be called"); }; const result = await Effect.gen(function* () { const rpc = yield* WorkflowRpc; return yield* rpc .execute( "workflow.capabilities.call", "http://127.0.0.1:8765/rpc", { qualified_name: "" }, ) .pipe(Effect.either); }).pipe(Effect.provide(makeWorkflowRpcLayer({ fetch })), Effect.runPromise); expect(Either.isLeft(result)).toBe(true); if (Either.isRight(result)) return; expect(result.left).toBeInstanceOf(RpcDecodeError); expect(fetchCalled).toBe(false); }); it("dispatches the exact method and interprets the result", async () => { const params = { qualified_name: "local.example.echo", payload: { text: "hello" }, deployment_id: "demo.default", }; const result = { qualified_name: "local.example.echo", source_id: "local.example", kind: "node_spec" as const, deployment_id: "demo.default", outcome: "ok", output: { text: "hello" }, diagnostics: [ { bound_source: "local.example", code: "ok", logical_ref: "local.example.echo", message: "called", repair_hint: null, severity: "info", }, ], }; const fetch: typeof globalThis.fetch = async (input, init) => { const request = await requestBody(input, init); expect(request.method).toBe("workflow.capabilities.call"); expect(request.params).toEqual(params); return jsonResponse({ jsonrpc: "2.0", id: request.id, result, }); }; const exchange = await runOperation( { fetch }, "workflow.capabilities.call", params, ); expect(exchange.interpreted).toEqual({ qualifiedName: "local.example.echo", sourceId: "local.example", kind: "node_spec", deploymentId: "demo.default", outcome: "ok", output: { text: "hello" }, diagnostics: [ { boundSource: "local.example", code: "ok", logicalRef: "local.example.echo", message: "called", repairHint: null, severity: "info", }, ], }); expect(exchange.equivalentCli).toBe( "uv run wf cap call local.example.echo --input '{\"text\":\"hello\"}' --deployment demo.default", ); }); });