fix: address console read client review findings

This commit is contained in:
lda
2026-08-04 20:12:31 +07:00 Verified
parent fcb96f0f62
commit 38b1cb8d03
8 changed files with 157 additions and 36 deletions
@@ -10,6 +10,40 @@ const executor = () =>
({ run: vi.fn().mockResolvedValue({}) }) as unknown as ConsoleReadExecutor;
describe("CapabilityClient", () => {
it("decodes the actual capability interpreter envelope at the client boundary", async () => {
const interpreterResult = {
capabilities: [
{
kind: "node_spec",
name: "local.lda_docs.read_documents",
sourceId: "local.lda_docs",
description: "Read selected project documents.",
outcomes: ["ok", "error"],
isAsync: false,
inputFields: ["names"],
outputFields: ["documents"],
},
],
nextCursor: null,
total: 1,
};
const run = vi.fn(
(
_operation: unknown,
_params: unknown,
decode: (value: unknown) => unknown,
) => Promise.resolve(decode(interpreterResult)),
);
const client = createCapabilityClient({ run } as unknown as ConsoleReadExecutor);
const page = await client.list({});
expect(page.capabilities[0]?.name).toBe(
"local.lda_docs.read_documents",
);
expect(page.total).toBe(1);
});
it("lowers list filters to the capability operation payload", async () => {
const readExecutor = executor();
const client = createCapabilityClient(readExecutor);
@@ -12,7 +12,7 @@ const wrapperHints = {
describe("capability models", () => {
it("preserves discriminated node and wrapper summaries", () => {
const page = decodeCapabilityPage({
items: [
capabilities: [
{
kind: "node_spec",
name: "local.docs.read",
@@ -21,6 +21,7 @@ describe("capability models", () => {
outcomes: ["ok", "error"],
inputFields: ["names"],
outputFields: ["documents"],
isAsync: false,
},
{
kind: "wrapper_artifact",
@@ -30,15 +31,19 @@ describe("capability models", () => {
outcomes: ["ok"],
inputFields: [],
outputFields: ["report"],
isAsync: true,
artifactId: "reports",
title: "Reports",
version: 2,
},
],
nextCursor: null,
total: 2,
});
expect(page.items[0]?.kind).toBe("node_spec");
expect(page.items[1]?.kind).toBe("wrapper_artifact");
expect(page.items[0]?.description).toBeNull();
expect(page.capabilities[0]?.kind).toBe("node_spec");
expect(page.capabilities[1]?.kind).toBe("wrapper_artifact");
expect(page.capabilities[0]?.description).toBeNull();
});
it("decodes nullable detail fields for both capability kinds", () => {
@@ -37,7 +37,7 @@ const CapabilitySummarySchema = v.variant("kind", [
]);
const CapabilityPageSchema = v.object({
items: v.array(CapabilitySummarySchema),
capabilities: v.array(CapabilitySummarySchema),
nextCursor: v.nullish(v.string(), null),
total: v.number(),
});
@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import { ConsoleApiError } from "../../connection/api.js";
import type { RpcResponse } from "../../connection/contracts.js";
import { createConsoleReadExecutor } from "./read-executor.js";
@@ -103,6 +104,28 @@ describe("ConsoleReadExecutor", () => {
expect(recordEvidence).toHaveBeenCalledTimes(1);
});
it.each([
["protocol", "malformed JSON response"],
["decode", "malformed response from server"],
] as const)(
"maps typed %s invocation failures to decode errors",
async (kind, message) => {
const recordEvidence = vi.fn();
const executor = createConsoleReadExecutor({
target: "http://console.test/rpc",
recordEvidence,
invoke: vi.fn(async () => {
throw new ConsoleApiError(kind, message);
}),
});
await expect(
executor.run("workflow.capabilities.list", {}, (value) => value),
).rejects.toMatchObject({ kind: "decode", message });
expect(recordEvidence).toHaveBeenCalledTimes(1);
},
);
it("keeps evidence ids unique across consecutive reads", async () => {
const recordEvidence = vi.fn();
const executor = createConsoleReadExecutor({
@@ -1,4 +1,4 @@
import { callOperation } from "../../connection/api.js";
import { callOperation, ConsoleApiError } from "../../connection/api.js";
import type {
OperationName,
RpcResponse,
@@ -37,6 +37,13 @@ const errorKindForCode = (code: string): ConsoleClientErrorKind => {
const errorMessage = (error: unknown): string =>
error instanceof Error ? error.message : String(error);
const clientErrorKindForInvocation = (
error: unknown,
): "decode" | "transport" =>
error instanceof ConsoleApiError && error.kind !== "transport"
? "decode"
: "transport";
export const createConsoleReadExecutor = (options: {
readonly target: string;
readonly recordEvidence: (record: EvidenceRecord) => void;
@@ -83,7 +90,7 @@ export const createConsoleReadExecutor = (options: {
0,
);
throw new ConsoleClientError(
"transport",
clientErrorKindForInvocation(error),
operation,
errorMessage(error),
);