@mindburn/helm-ai-kernel 0.5.1 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/adapters/agent-frameworks.coverage.test.d.ts +1 -0
- package/dist/adapters/agent-frameworks.coverage.test.js +50 -0
- package/dist/client-coverage.test.d.ts +1 -0
- package/dist/client-coverage.test.js +152 -0
- package/dist/generated-coverage.test.d.ts +1 -0
- package/dist/generated-coverage.test.js +284 -0
- package/dist/types.gen.d.ts +5635 -823
- package/dist/types.gen.js +3194 -264
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm ci
|
|
|
10
10
|
npm run build
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Package metadata declares version `0.5.
|
|
13
|
+
Package metadata declares version `0.5.9` in `package.json`; this README does
|
|
14
14
|
not claim that a registry package has been published.
|
|
15
15
|
|
|
16
16
|
## Local Development
|
|
@@ -86,4 +86,4 @@ and sandbox grants attached to HELM-native receipts and EvidencePacks.
|
|
|
86
86
|
|
|
87
87
|
## Release Notes
|
|
88
88
|
|
|
89
|
-
`0.5.
|
|
89
|
+
`0.5.9` is the release-hardening patch with the retained OpenAPI client surface and protobuf message bindings.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createAgentFrameworkAdapter, fromLangGraphToolCall, fromLlamaIndexToolCall, fromN8NNodeExecution, fromOpenAIAgentsToolCall, fromSemanticKernelFunctionCall, fromZapierWebhookCall, fromRawMCPToolCall, toOpenAIFunctionTool, } from "./agent-frameworks.js";
|
|
3
|
+
describe("agent framework adapter coverage branches", () => {
|
|
4
|
+
it("normalizes fallback argument shapes", () => {
|
|
5
|
+
expect(fromOpenAIAgentsToolCall({ name: "tool", arguments: "not json" }).arguments).toEqual({ value: "not json" });
|
|
6
|
+
expect(fromOpenAIAgentsToolCall({ name: "tool", arguments: "" }).arguments).toEqual({});
|
|
7
|
+
expect(fromOpenAIAgentsToolCall({ name: "tool", arguments: [1, 2] }).arguments).toEqual({ items: [1, 2] });
|
|
8
|
+
expect(fromOpenAIAgentsToolCall({ name: "tool", arguments: 7 }).arguments).toEqual({ value: 7 });
|
|
9
|
+
expect(fromSemanticKernelFunctionCall({ pluginName: "files", functionName: "read", arguments: {} }).toolName).toBe("files.read");
|
|
10
|
+
expect(fromSemanticKernelFunctionCall({ functionName: "read", arguments: {} }).toolName).toBe("read");
|
|
11
|
+
expect(fromLlamaIndexToolCall({ tool_name: "search", input: null }).arguments).toEqual({});
|
|
12
|
+
expect(fromN8NNodeExecution({ name: "http", input: { url: "https://example.test" } }).toolName).toBe("http");
|
|
13
|
+
expect(fromZapierWebhookCall({ tool: "zap", payload: { id: 1 } }).toolName).toBe("zap");
|
|
14
|
+
});
|
|
15
|
+
it("throws on missing and unnormalizable tool names", () => {
|
|
16
|
+
expect(() => fromLangGraphToolCall({ args: {} })).toThrow("requires a tool name");
|
|
17
|
+
expect(() => toOpenAIFunctionTool({
|
|
18
|
+
framework: "raw-mcp",
|
|
19
|
+
toolName: " ",
|
|
20
|
+
arguments: {},
|
|
21
|
+
})).toThrow("normalizes to an empty");
|
|
22
|
+
});
|
|
23
|
+
it("builds and submits with default option merging", async () => {
|
|
24
|
+
const response = { response: { id: "chatcmpl", choices: [] }, governance: { receiptId: "r1" } };
|
|
25
|
+
const client = {
|
|
26
|
+
chatCompletionsWithReceipt: vi.fn(async () => response),
|
|
27
|
+
};
|
|
28
|
+
const adapter = createAgentFrameworkAdapter(client, {
|
|
29
|
+
model: "gpt-default",
|
|
30
|
+
policyPrompt: "default policy",
|
|
31
|
+
temperature: 0.1,
|
|
32
|
+
maxTokens: 128,
|
|
33
|
+
metadata: { defaulted: true },
|
|
34
|
+
});
|
|
35
|
+
const action = fromRawMCPToolCall({
|
|
36
|
+
serverId: "srv",
|
|
37
|
+
name: "file.read",
|
|
38
|
+
args: { path: "/tmp/a" },
|
|
39
|
+
scopes: ["fs:read"],
|
|
40
|
+
metadata: { call: "one" },
|
|
41
|
+
});
|
|
42
|
+
const request = adapter.buildRequest(action);
|
|
43
|
+
expect(request.model).toBe("gpt-default");
|
|
44
|
+
expect(request.messages[1].content).toContain("file.read");
|
|
45
|
+
const result = await adapter.submit(action, { model: "gpt-override", temperature: 0.2 });
|
|
46
|
+
expect(result.request.model).toBe("gpt-override");
|
|
47
|
+
expect(result.action.metadata).toMatchObject({ defaulted: true, call: "one" });
|
|
48
|
+
expect(client.chatCompletionsWithReceipt).toHaveBeenCalledOnce();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { HelmApiError, HelmClient } from "./client.js";
|
|
3
|
+
function jsonResponse(body, status = 200, headers = {}) {
|
|
4
|
+
return new Response(JSON.stringify(body), {
|
|
5
|
+
status,
|
|
6
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function helmError(status = 500) {
|
|
10
|
+
return jsonResponse({ error: { message: "denied", type: "policy", code: "DENY", reason_code: "DENY_POLICY_VIOLATION" } }, status);
|
|
11
|
+
}
|
|
12
|
+
describe("HelmClient coverage matrix", () => {
|
|
13
|
+
let fetchSpy;
|
|
14
|
+
let client;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
fetchSpy = vi.fn(async () => jsonResponse({ ok: true }));
|
|
17
|
+
vi.stubGlobal("fetch", fetchSpy);
|
|
18
|
+
client = new HelmClient({
|
|
19
|
+
baseUrl: "http://helm.test/",
|
|
20
|
+
apiKey: "token",
|
|
21
|
+
tenantId: "tenant-a",
|
|
22
|
+
timeout: 5_000,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
it("constructs fallback API errors for non-HELM bodies", () => {
|
|
29
|
+
expect(new HelmApiError(418, {}).message).toBe("HELM API request failed with HTTP 418");
|
|
30
|
+
expect(new HelmApiError(409, { error: null }).reasonCode).toBe("ERROR_INTERNAL");
|
|
31
|
+
expect(new HelmApiError(400, { error: { message: "bad" } }).message).toBe("HELM API request failed with HTTP 400");
|
|
32
|
+
expect(new HelmApiError(401, { error: { reason_code: "DENY" } }).reasonCode).toBe("ERROR_INTERNAL");
|
|
33
|
+
});
|
|
34
|
+
it("exercises every JSON endpoint wrapper", async () => {
|
|
35
|
+
const calls = [
|
|
36
|
+
["evaluateDecision", [{ effect: "read" }]],
|
|
37
|
+
["runPublicDemo", ["read_ticket", { id: 1 }]],
|
|
38
|
+
["verifyPublicDemoReceipt", [{ receipt_id: "r1" }, "hash"]],
|
|
39
|
+
["approveIntent", [{ intent_hash: "h", signature_b64: "sig", public_key_b64: "pk" }]],
|
|
40
|
+
["listSessions", []],
|
|
41
|
+
["listSessions", [7, 3]],
|
|
42
|
+
["getReceipts", ["session/one"]],
|
|
43
|
+
["getReceipt", ["receipt#one"]],
|
|
44
|
+
["createEvidenceEnvelopeManifest", [{ manifest_id: "m1", envelope: "dsse", native_evidence_hash: "h" }]],
|
|
45
|
+
["listEvidenceEnvelopeManifests", []],
|
|
46
|
+
["getEvidenceEnvelopeManifest", ["manifest/a b"]],
|
|
47
|
+
["verifyEvidenceEnvelopeManifest", ["manifest/a b"]],
|
|
48
|
+
["getEvidenceEnvelopePayload", ["manifest/a b"]],
|
|
49
|
+
["getBoundaryStatus", []],
|
|
50
|
+
["listBoundaryCapabilities", []],
|
|
51
|
+
["listBoundaryRecords", []],
|
|
52
|
+
["listBoundaryRecords", [{ limit: 10, offset: 0, empty: "", omitted: undefined }]],
|
|
53
|
+
["getBoundaryRecord", ["record/a b"]],
|
|
54
|
+
["verifyBoundaryRecord", ["record/a b"]],
|
|
55
|
+
["listBoundaryCheckpoints", []],
|
|
56
|
+
["createBoundaryCheckpoint", []],
|
|
57
|
+
["verifyBoundaryCheckpoint", ["checkpoint/a b"]],
|
|
58
|
+
["conformanceRun", [{ level: "basic" }]],
|
|
59
|
+
["getConformanceReport", ["report-1"]],
|
|
60
|
+
["listConformanceReports", []],
|
|
61
|
+
["listConformanceVectors", []],
|
|
62
|
+
["listNegativeConformanceVectors", []],
|
|
63
|
+
["listMcpRegistry", []],
|
|
64
|
+
["discoverMcpServer", [{ server_id: "srv" }]],
|
|
65
|
+
["approveMcpServer", [{ server_id: "srv", approver_id: "me", approval_receipt_id: "r" }]],
|
|
66
|
+
["getMcpRegistryRecord", ["srv/a b"]],
|
|
67
|
+
["approveMcpRegistryRecord", ["srv/a b", { reason: "ok" }]],
|
|
68
|
+
["revokeMcpRegistryRecord", ["srv/a b"]],
|
|
69
|
+
["revokeMcpRegistryRecord", ["srv/a b", "stale"]],
|
|
70
|
+
["scanMcpServer", [{ server_id: "srv" }]],
|
|
71
|
+
["listMcpAuthProfiles", []],
|
|
72
|
+
["putMcpAuthProfile", ["profile/a b", { scopes: ["tools"] }]],
|
|
73
|
+
["authorizeMcpCall", [{ tool: "read" }]],
|
|
74
|
+
["inspectSandboxGrants", []],
|
|
75
|
+
["inspectSandboxGrants", ["runtime", "profile", "epoch"]],
|
|
76
|
+
["listSandboxProfiles", []],
|
|
77
|
+
["listSandboxGrants", []],
|
|
78
|
+
["createSandboxGrant", [{ runtime: "wasi" }]],
|
|
79
|
+
["getSandboxGrant", ["grant/a b"]],
|
|
80
|
+
["verifySandboxGrant", ["grant/a b"]],
|
|
81
|
+
["preflightSandboxGrant", [{ runtime: "wasi" }]],
|
|
82
|
+
["listAgentIdentities", []],
|
|
83
|
+
["getAuthzHealth", []],
|
|
84
|
+
["checkAuthz", [{ subject: "agent" }]],
|
|
85
|
+
["listAuthzSnapshots", []],
|
|
86
|
+
["getAuthzSnapshot", ["snapshot/a b"]],
|
|
87
|
+
["listApprovalCeremonies", []],
|
|
88
|
+
["createApprovalCeremony", [{ approval_id: "a1" }]],
|
|
89
|
+
["transitionApprovalCeremony", ["approval/a b", "approve"]],
|
|
90
|
+
["transitionApprovalCeremony", ["approval/a b", "deny", { reason: "bad" }]],
|
|
91
|
+
["createApprovalWebAuthnChallenge", ["approval/a b"]],
|
|
92
|
+
["createApprovalWebAuthnChallenge", ["approval/a b", { user: "me" }]],
|
|
93
|
+
["assertApprovalWebAuthnChallenge", ["approval/a b", { credential: "c" }]],
|
|
94
|
+
["listBudgetCeilings", []],
|
|
95
|
+
["putBudgetCeiling", ["budget/a b", { limit: 1 }]],
|
|
96
|
+
["getCoexistenceCapabilities", []],
|
|
97
|
+
["getTelemetryOTelConfig", []],
|
|
98
|
+
["exportTelemetry", [{ span: "all" }]],
|
|
99
|
+
["health", []],
|
|
100
|
+
["version", []],
|
|
101
|
+
];
|
|
102
|
+
for (const [method, args] of calls) {
|
|
103
|
+
await client[method](...args);
|
|
104
|
+
}
|
|
105
|
+
expect(fetchSpy).toHaveBeenCalledTimes(calls.length);
|
|
106
|
+
expect(fetchSpy.mock.calls.some(([url]) => String(url).includes("limit=10&offset=0"))).toBe(true);
|
|
107
|
+
expect(fetchSpy.mock.calls.some(([url]) => String(url).includes("runtime=runtime&profile=profile&policy_epoch=epoch"))).toBe(true);
|
|
108
|
+
expect(fetchSpy.mock.calls.every(([, init]) => init.headers.Authorization === "Bearer token")).toBe(true);
|
|
109
|
+
expect(fetchSpy.mock.calls.every(([, init]) => init.headers["X-Helm-Tenant-ID"] === "tenant-a")).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it("extracts governance headers and default values", async () => {
|
|
112
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse({ id: "chatcmpl", choices: [] }, 200, {
|
|
113
|
+
"X-Helm-Receipt-ID": "r1",
|
|
114
|
+
"X-Helm-Status": "ALLOW",
|
|
115
|
+
"X-Helm-Output-Hash": "oh",
|
|
116
|
+
"X-Helm-Lamport-Clock": "9",
|
|
117
|
+
"X-Helm-Reason-Code": "ALLOW",
|
|
118
|
+
"X-Helm-Decision-ID": "d1",
|
|
119
|
+
"X-Helm-ProofGraph-Node": "pg",
|
|
120
|
+
"X-Helm-Signature": "sig",
|
|
121
|
+
"X-Helm-Tool-Calls": "2",
|
|
122
|
+
}));
|
|
123
|
+
await expect(client.chatCompletionsWithReceipt({ model: "gpt", messages: [] })).resolves.toMatchObject({
|
|
124
|
+
governance: { receiptId: "r1", lamportClock: 9, toolCalls: 2 },
|
|
125
|
+
});
|
|
126
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse({ id: "chatcmpl", choices: [] }));
|
|
127
|
+
await expect(client.chatCompletionsWithReceipt({ model: "gpt", messages: [] })).resolves.toMatchObject({
|
|
128
|
+
governance: { receiptId: "", lamportClock: 0, toolCalls: 0 },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
it("covers binary and form endpoints including error branches", async () => {
|
|
132
|
+
const blob = new Blob(["bundle"]);
|
|
133
|
+
fetchSpy.mockResolvedValueOnce(new Response("tgz", { status: 200 }));
|
|
134
|
+
await expect(client.exportEvidence("session-1")).resolves.toBeInstanceOf(Blob);
|
|
135
|
+
fetchSpy.mockResolvedValueOnce(helmError(400));
|
|
136
|
+
await expect(client.exportEvidence()).rejects.toThrow(HelmApiError);
|
|
137
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse({ verdict: "PASS" }));
|
|
138
|
+
await expect(client.verifyEvidence(blob)).resolves.toMatchObject({ verdict: "PASS" });
|
|
139
|
+
fetchSpy.mockResolvedValueOnce(helmError(422));
|
|
140
|
+
await expect(client.verifyEvidence(blob)).rejects.toThrow(HelmApiError);
|
|
141
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse({ verdict: "PASS" }));
|
|
142
|
+
await expect(client.replayVerify(blob)).resolves.toMatchObject({ verdict: "PASS" });
|
|
143
|
+
fetchSpy.mockResolvedValueOnce(helmError(422));
|
|
144
|
+
await expect(client.replayVerify(blob)).rejects.toThrow(HelmApiError);
|
|
145
|
+
});
|
|
146
|
+
it("throws for JSON request and receipt failures", async () => {
|
|
147
|
+
fetchSpy.mockResolvedValueOnce(helmError(403));
|
|
148
|
+
await expect(client.health()).rejects.toThrow(HelmApiError);
|
|
149
|
+
fetchSpy.mockResolvedValueOnce(helmError(403));
|
|
150
|
+
await expect(client.chatCompletionsWithReceipt({ model: "gpt", messages: [] })).rejects.toThrow(HelmApiError);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import * as openapiTypes from "./types.gen.js";
|
|
4
|
+
import * as durationProto from "./generated/google/protobuf/duration.js";
|
|
5
|
+
import * as timestampProto from "./generated/google/protobuf/timestamp.js";
|
|
6
|
+
import * as authorityProto from "./generated/helm/authority/v1/authority.js";
|
|
7
|
+
import * as effectsProto from "./generated/helm/effects/v1/effects.js";
|
|
8
|
+
import * as interventionProto from "./generated/helm/intervention/v1/intervention.js";
|
|
9
|
+
import * as kernelProto from "./generated/helm/kernel/v1/helm.js";
|
|
10
|
+
import * as truthProto from "./generated/helm/truth/v1/truth.js";
|
|
11
|
+
const typeSource = readFileSync(new URL("types.gen.ts", import.meta.url), "utf8");
|
|
12
|
+
function requiredFieldsFor(modelName) {
|
|
13
|
+
const match = typeSource.match(instanceOfPattern(modelName));
|
|
14
|
+
if (!match)
|
|
15
|
+
return [];
|
|
16
|
+
return [...match[0].matchAll(/if \(!\('([^']+)' in value\)\) return false;/g)].map((field) => field[1]);
|
|
17
|
+
}
|
|
18
|
+
function instanceOfPattern(modelName) {
|
|
19
|
+
return new RegExp(`export function instanceOf${modelName}[^]*?\\n}\\n\\nexport function ${modelName}FromJSON`);
|
|
20
|
+
}
|
|
21
|
+
function converterBodyFor(modelName) {
|
|
22
|
+
const match = typeSource.match(new RegExp(`export function ${modelName}FromJSONTyped[^]*?\\n}\\n\\nexport function ${modelName}ToJSON`));
|
|
23
|
+
return match?.[0] ?? "";
|
|
24
|
+
}
|
|
25
|
+
function normalizeJson(value) {
|
|
26
|
+
if (value == null)
|
|
27
|
+
return value;
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.map(normalizeJson);
|
|
30
|
+
if (typeof value !== "object")
|
|
31
|
+
return value;
|
|
32
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeJson(item)]));
|
|
33
|
+
}
|
|
34
|
+
function sourceValueFor(modelName, field, expression, stack) {
|
|
35
|
+
const arrayMatch = expression.match(/json\['[^']+'\] as Array<any>\)\.map\(([^)]+)FromJSON\)/);
|
|
36
|
+
if (arrayMatch) {
|
|
37
|
+
return [sourceSampleForModel(arrayMatch[1], false, stack)];
|
|
38
|
+
}
|
|
39
|
+
const nestedMatch = expression.match(/([A-Za-z0-9_]+)FromJSON\(json\['[^']+'\]\)/);
|
|
40
|
+
if (nestedMatch) {
|
|
41
|
+
return sourceSampleForModel(nestedMatch[1], false, stack);
|
|
42
|
+
}
|
|
43
|
+
if (expression.includes("new Date"))
|
|
44
|
+
return "2026-01-01T00:00:00.000Z";
|
|
45
|
+
if (expression.includes("bytesFromBase64"))
|
|
46
|
+
return "aGVsbQ==";
|
|
47
|
+
if (/count|number|lamport|clock|port|limit|size|ttl|bytes|cpu|memory|score|confidence|version/i.test(field))
|
|
48
|
+
return 1;
|
|
49
|
+
if (/enabled|allow|deny|active|valid|ok|success|required|experimental|hosted|launchable|verified/i.test(field))
|
|
50
|
+
return true;
|
|
51
|
+
return "sample";
|
|
52
|
+
}
|
|
53
|
+
function sourceSampleForModel(modelName, requiredOnly = false, stack = []) {
|
|
54
|
+
if (stack.includes(modelName))
|
|
55
|
+
return {};
|
|
56
|
+
if (modelName === "MCPJSONRPCRequestId" || modelName === "MCPJSONRPCResponseId")
|
|
57
|
+
return 1;
|
|
58
|
+
if (modelName === "MCPToolCallResponseResult")
|
|
59
|
+
return { content: "sample" };
|
|
60
|
+
if (modelName === "SandboxGrantInspection")
|
|
61
|
+
return [];
|
|
62
|
+
const body = converterBodyFor(modelName);
|
|
63
|
+
const required = new Set(requiredFieldsFor(modelName));
|
|
64
|
+
const sample = {};
|
|
65
|
+
for (const match of body.matchAll(/'([^']+)': ([^\n]+),/g)) {
|
|
66
|
+
const [, field, expression] = match;
|
|
67
|
+
if (requiredOnly && required.size > 0 && !required.has(field))
|
|
68
|
+
continue;
|
|
69
|
+
sample[field] = sourceValueFor(modelName, field, expression, [...stack, modelName]);
|
|
70
|
+
}
|
|
71
|
+
return sample;
|
|
72
|
+
}
|
|
73
|
+
function sampleForModel(modelName, requiredOnly = false) {
|
|
74
|
+
if (modelName === "MCPJSONRPCRequestId" || modelName === "MCPJSONRPCResponseId") {
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
if (modelName === "MCPToolCallResponseResult") {
|
|
78
|
+
return { content: "sample" };
|
|
79
|
+
}
|
|
80
|
+
if (modelName === "SandboxGrantInspection") {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return sourceSampleForModel(modelName, requiredOnly);
|
|
84
|
+
}
|
|
85
|
+
function modelNames() {
|
|
86
|
+
return [...typeSource.matchAll(/^export function ([A-Za-z0-9_]+)FromJSON\(/gm)]
|
|
87
|
+
.map((match) => match[1])
|
|
88
|
+
.filter((name) => typeof openapiTypes[`${name}FromJSON`] === "function")
|
|
89
|
+
.sort();
|
|
90
|
+
}
|
|
91
|
+
function expectNoThrow(fn) {
|
|
92
|
+
try {
|
|
93
|
+
fn();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
expect(error).toBeUndefined();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
describe("generated OpenAPI TypeScript helpers", () => {
|
|
100
|
+
it("exercises every generated JSON converter and interface guard", () => {
|
|
101
|
+
const exercised = [];
|
|
102
|
+
for (const name of modelNames()) {
|
|
103
|
+
const fromJSON = openapiTypes[`${name}FromJSON`];
|
|
104
|
+
const fromJSONTyped = openapiTypes[`${name}FromJSONTyped`];
|
|
105
|
+
const toJSON = openapiTypes[`${name}ToJSON`];
|
|
106
|
+
const instanceOf = openapiTypes[`instanceOf${name}`];
|
|
107
|
+
expect(fromJSON(null)).toBeNull();
|
|
108
|
+
expectNoThrow(() => fromJSONTyped(null, true));
|
|
109
|
+
if (toJSON) {
|
|
110
|
+
expect(toJSON(null)).toBeNull();
|
|
111
|
+
expect(toJSON(undefined)).toBeUndefined();
|
|
112
|
+
}
|
|
113
|
+
for (const requiredOnly of [false, true]) {
|
|
114
|
+
const sample = sampleForModel(name, requiredOnly);
|
|
115
|
+
const converted = fromJSON(sample);
|
|
116
|
+
expectNoThrow(() => fromJSONTyped(sample, requiredOnly));
|
|
117
|
+
if (toJSON) {
|
|
118
|
+
expectNoThrow(() => toJSON(converted));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (name === "SandboxGrantInspection") {
|
|
122
|
+
const grant = sampleForModel("SandboxGrant");
|
|
123
|
+
const converted = fromJSON(grant);
|
|
124
|
+
expectNoThrow(() => fromJSONTyped(grant, true));
|
|
125
|
+
expectNoThrow(() => toJSON(converted));
|
|
126
|
+
}
|
|
127
|
+
if (instanceOf) {
|
|
128
|
+
const required = requiredFieldsFor(name);
|
|
129
|
+
const full = { ...sampleForModel(name), ...Object.fromEntries(required.map((field) => [field, "sample"])) };
|
|
130
|
+
expect(instanceOf(full)).toBe(true);
|
|
131
|
+
for (const field of required) {
|
|
132
|
+
const missing = { ...full };
|
|
133
|
+
delete missing[field];
|
|
134
|
+
expect(instanceOf(missing)).toBe(false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exercised.push(name);
|
|
138
|
+
}
|
|
139
|
+
expect(exercised.length).toBeGreaterThan(150);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
const protoModules = [
|
|
143
|
+
{ exports: durationProto, source: readFileSync(new URL("generated/google/protobuf/duration.ts", import.meta.url), "utf8") },
|
|
144
|
+
{ exports: timestampProto, source: readFileSync(new URL("generated/google/protobuf/timestamp.ts", import.meta.url), "utf8") },
|
|
145
|
+
{ exports: authorityProto, source: readFileSync(new URL("generated/helm/authority/v1/authority.ts", import.meta.url), "utf8") },
|
|
146
|
+
{ exports: effectsProto, source: readFileSync(new URL("generated/helm/effects/v1/effects.ts", import.meta.url), "utf8") },
|
|
147
|
+
{ exports: interventionProto, source: readFileSync(new URL("generated/helm/intervention/v1/intervention.ts", import.meta.url), "utf8") },
|
|
148
|
+
{ exports: kernelProto, source: readFileSync(new URL("generated/helm/kernel/v1/helm.ts", import.meta.url), "utf8") },
|
|
149
|
+
{ exports: truthProto, source: readFileSync(new URL("generated/helm/truth/v1/truth.ts", import.meta.url), "utf8") },
|
|
150
|
+
];
|
|
151
|
+
function isMessageFns(value) {
|
|
152
|
+
return typeof value === "object"
|
|
153
|
+
&& value !== null
|
|
154
|
+
&& ["encode", "decode", "fromJSON", "toJSON", "create", "fromPartial"].every((key) => typeof value[key] === "function");
|
|
155
|
+
}
|
|
156
|
+
function messageBlock(source, messageName) {
|
|
157
|
+
const match = source.match(new RegExp(`export const ${messageName}: MessageFns<${messageName}> = \\{[^]*?\\n\\};`));
|
|
158
|
+
return match?.[0] ?? "";
|
|
159
|
+
}
|
|
160
|
+
function nestedRepeatedFields(source, messageName) {
|
|
161
|
+
return [...messageBlock(source, messageName).matchAll(/message\.(\w+) = object\.\1\?\.map\(\(e\) => ([A-Za-z0-9_]+)\.fromPartial\(e\)\) \|\| \[\];/g)]
|
|
162
|
+
.map((match) => [match[1], match[2]]);
|
|
163
|
+
}
|
|
164
|
+
function primitiveRepeatedFields(source, messageName) {
|
|
165
|
+
return [...messageBlock(source, messageName).matchAll(/message\.(\w+) = object\.\1\?\.map\(\(e\) => e\) \|\| \[\];/g)]
|
|
166
|
+
.map((match) => match[1]);
|
|
167
|
+
}
|
|
168
|
+
function nestedOptionalFields(source, messageName) {
|
|
169
|
+
return [...messageBlock(source, messageName).matchAll(/message\.(\w+) = \(object\.\1 !== undefined && object\.\1 !== null\)\s+\? ([A-Za-z0-9_]+)\.fromPartial/g)]
|
|
170
|
+
.map((match) => [match[1], match[2]]);
|
|
171
|
+
}
|
|
172
|
+
function nonDefaultValueFor(key, value) {
|
|
173
|
+
if (typeof value === "string")
|
|
174
|
+
return `${key}-sample`;
|
|
175
|
+
if (typeof value === "number")
|
|
176
|
+
return 1;
|
|
177
|
+
if (typeof value === "boolean")
|
|
178
|
+
return true;
|
|
179
|
+
if (value instanceof Uint8Array)
|
|
180
|
+
return new Uint8Array([1, 2, 3]);
|
|
181
|
+
if (Array.isArray(value))
|
|
182
|
+
return ["sample"];
|
|
183
|
+
if (value === undefined && /(time|date|deadline|expires|issued|created|updated|completed|timestamp)/i.test(key)) {
|
|
184
|
+
return new Date("2026-01-01T00:00:00.000Z");
|
|
185
|
+
}
|
|
186
|
+
if (value && typeof value === "object")
|
|
187
|
+
return { key: "value" };
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function populatedMessage(name, fns, moduleExports, source, stack = []) {
|
|
191
|
+
const base = fns.create();
|
|
192
|
+
const populated = Object.fromEntries(Object.entries(base).map(([key, value]) => [key, nonDefaultValueFor(key, value)]));
|
|
193
|
+
for (const [field, nestedName] of nestedOptionalFields(source, name)) {
|
|
194
|
+
const nested = moduleExports[nestedName];
|
|
195
|
+
if (isMessageFns(nested) && !stack.includes(nestedName)) {
|
|
196
|
+
populated[field] = populatedMessage(nestedName, nested, moduleExports, source, [...stack, name]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const [field, nestedName] of nestedRepeatedFields(source, name)) {
|
|
200
|
+
const nested = moduleExports[nestedName];
|
|
201
|
+
if (isMessageFns(nested) && !stack.includes(nestedName)) {
|
|
202
|
+
populated[field] = [populatedMessage(nestedName, nested, moduleExports, source, [...stack, name])];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const field of primitiveRepeatedFields(source, name)) {
|
|
206
|
+
populated[field] = ["sample"];
|
|
207
|
+
}
|
|
208
|
+
return populated;
|
|
209
|
+
}
|
|
210
|
+
function snakeCaseKeys(value) {
|
|
211
|
+
if (value == null || typeof value !== "object" || value instanceof Date || value instanceof Uint8Array)
|
|
212
|
+
return value;
|
|
213
|
+
if (Array.isArray(value))
|
|
214
|
+
return value.map(snakeCaseKeys);
|
|
215
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
216
|
+
key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`),
|
|
217
|
+
snakeCaseKeys(item),
|
|
218
|
+
]));
|
|
219
|
+
}
|
|
220
|
+
function serviceMessageName(fn) {
|
|
221
|
+
return String(fn).match(/([A-Za-z0-9_]+)\.encode/)?.[1];
|
|
222
|
+
}
|
|
223
|
+
describe("generated protobuf TypeScript helpers", () => {
|
|
224
|
+
it("exercises message function tables and enum JSON helpers", () => {
|
|
225
|
+
let messageCount = 0;
|
|
226
|
+
let enumHelperCount = 0;
|
|
227
|
+
for (const { exports: moduleExports, source } of protoModules) {
|
|
228
|
+
for (const [name, value] of Object.entries(moduleExports)) {
|
|
229
|
+
if (isMessageFns(value)) {
|
|
230
|
+
const empty = value.create();
|
|
231
|
+
const populated = populatedMessage(name, value, moduleExports, source);
|
|
232
|
+
const emptyBytes = value.encode(empty).finish();
|
|
233
|
+
const fullBytes = value.encode(populated).finish();
|
|
234
|
+
expectNoThrow(() => value.decode(emptyBytes));
|
|
235
|
+
expectNoThrow(() => value.decode(fullBytes));
|
|
236
|
+
expectNoThrow(() => value.decode(fullBytes, fullBytes.length));
|
|
237
|
+
expectNoThrow(() => value.fromJSON({}));
|
|
238
|
+
expectNoThrow(() => value.fromJSON(value.toJSON(populated)));
|
|
239
|
+
expectNoThrow(() => value.fromJSON(snakeCaseKeys(value.toJSON(populated))));
|
|
240
|
+
expectNoThrow(() => value.toJSON(empty));
|
|
241
|
+
expectNoThrow(() => value.toJSON(populated));
|
|
242
|
+
expectNoThrow(() => value.fromPartial({}));
|
|
243
|
+
expectNoThrow(() => value.fromPartial(populated));
|
|
244
|
+
messageCount += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (name.endsWith("FromJSON") && typeof value === "function") {
|
|
248
|
+
const enumName = name.slice(0, -"FromJSON".length);
|
|
249
|
+
const toJSON = moduleExports[`${enumName}ToJSON`];
|
|
250
|
+
for (const candidate of [0, 1, -1, "UNRECOGNIZED", "not-a-real-value"]) {
|
|
251
|
+
const converted = value(candidate);
|
|
252
|
+
if (toJSON) {
|
|
253
|
+
expect(typeof toJSON(converted)).toBe("string");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
enumHelperCount += 1;
|
|
257
|
+
}
|
|
258
|
+
if (typeof value === "object" && value !== null && !isMessageFns(value)) {
|
|
259
|
+
for (const descriptor of Object.values(value)) {
|
|
260
|
+
if (descriptor
|
|
261
|
+
&& typeof descriptor.requestSerialize === "function"
|
|
262
|
+
&& typeof descriptor.requestDeserialize === "function"
|
|
263
|
+
&& typeof descriptor.responseSerialize === "function"
|
|
264
|
+
&& typeof descriptor.responseDeserialize === "function") {
|
|
265
|
+
const requestName = serviceMessageName(descriptor.requestSerialize);
|
|
266
|
+
const responseName = serviceMessageName(descriptor.responseSerialize);
|
|
267
|
+
const exportsByName = moduleExports;
|
|
268
|
+
const requestFns = requestName ? exportsByName[requestName] : undefined;
|
|
269
|
+
const responseFns = responseName ? exportsByName[responseName] : undefined;
|
|
270
|
+
if (isMessageFns(requestFns) && isMessageFns(responseFns)) {
|
|
271
|
+
const requestBytes = descriptor.requestSerialize(populatedMessage(requestName, requestFns, moduleExports, source));
|
|
272
|
+
const responseBytes = descriptor.responseSerialize(populatedMessage(responseName, responseFns, moduleExports, source));
|
|
273
|
+
expectNoThrow(() => descriptor.requestDeserialize(requestBytes));
|
|
274
|
+
expectNoThrow(() => descriptor.responseDeserialize(responseBytes));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
expect(messageCount).toBeGreaterThan(30);
|
|
282
|
+
expect(enumHelperCount).toBeGreaterThan(5);
|
|
283
|
+
});
|
|
284
|
+
});
|