feat(shared): define harness protocols
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as shared from "../src/index.js";
|
||||
|
||||
describe("createApiError", () => {
|
||||
it("returns the uniform safe error envelope", () => {
|
||||
const factory = Reflect.get(shared, "createApiError") as ((code: string, message: string, requestId: string, details?: Record<string, unknown>) => unknown) | undefined;
|
||||
expect(factory, "createApiError must be exported").toBeDefined();
|
||||
expect(factory?.("SESSION_NOT_FOUND", "Session not found", "req_1", { sessionId: "ses_1" })).toEqual({
|
||||
error: {
|
||||
code: "SESSION_NOT_FOUND",
|
||||
message: "Session not found",
|
||||
requestId: "req_1",
|
||||
details: { sessionId: "ses_1" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed envelopes", () => {
|
||||
const candidate = Reflect.get(shared, "ApiErrorSchema") as { safeParse(value: unknown): { success: boolean } } | undefined;
|
||||
expect(candidate, "ApiErrorSchema must be exported").toBeDefined();
|
||||
expect(candidate?.safeParse({ error: { code: "", message: "bad" } }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as shared from "../src/index.js";
|
||||
|
||||
function schema(): { parse(value: unknown): unknown; safeParse(value: unknown): { success: boolean } } {
|
||||
const candidate = Reflect.get(shared, "HarnessEventSchema");
|
||||
expect(candidate, "HarnessEventSchema must be exported").toBeDefined();
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const base = {
|
||||
eventId: "evt_1",
|
||||
sessionId: "ses_1",
|
||||
timestamp: "2026-09-07T00:00:00.000Z",
|
||||
sequence: 1,
|
||||
};
|
||||
|
||||
describe("HarnessEventSchema", () => {
|
||||
it("accepts every required event discriminant", () => {
|
||||
const payloads: Record<string, unknown> = {
|
||||
"session.started": { cwd: "workspace/sessions/ses_1" },
|
||||
"session.ended": { reason: "deleted" },
|
||||
"user.message": { messageId: "msg_1", text: "hello", attachmentIds: [] },
|
||||
"assistant.delta": { messageId: "msg_2", delta: "hel" },
|
||||
"assistant.completed": { messageId: "msg_2" },
|
||||
"skill.requested": { name: "ads-analysis", source: "project" },
|
||||
"skill.loaded": { name: "ads-analysis", source: "project" },
|
||||
"tool.requested": { toolCallId: "call_1", toolName: "mock_ads_metrics", arguments: {} },
|
||||
"tool.completed": { toolCallId: "call_1", toolName: "mock_ads_metrics", result: {} },
|
||||
"tool.failed": { toolCallId: "call_1", toolName: "mock_ads_metrics", error: "failed" },
|
||||
"approval.required": { approvalId: "ap_1", toolCallId: "call_1", toolName: "save_report_draft", riskLevel: "medium", expiresAt: "2026-09-07T00:01:00.000Z", arguments: {} },
|
||||
"approval.resolved": { approvalId: "ap_1", status: "approved" },
|
||||
"attachment.uploaded": { attachmentId: "att_1", name: "creative.png", mediaType: "image/png", byteSize: 12, width: 1, height: 1 },
|
||||
"attachment.rejected": { name: "bad.svg", reason: "UNSUPPORTED_IMAGE" },
|
||||
"model.configured": { providerId: "openai", modelId: "gpt-image", supportsImages: true, source: "session" },
|
||||
"model.validation_failed": { code: "MODEL_CONNECTION_FAILED", message: "Connection failed" },
|
||||
"run.started": { modelId: "gpt-image" },
|
||||
"run.cancelled": { reason: "user" },
|
||||
"run.completed": { messageId: "msg_2" },
|
||||
"run.failed": { code: "MODEL_ERROR", message: "Run failed" },
|
||||
};
|
||||
|
||||
for (const [type, payload] of Object.entries(payloads)) {
|
||||
expect(schema().safeParse({ ...base, type, payload }).success, type).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a payload belonging to another discriminant", () => {
|
||||
expect(schema().safeParse({
|
||||
...base,
|
||||
type: "assistant.delta",
|
||||
payload: { approvalId: "ap_1" },
|
||||
}).success).toBe(false);
|
||||
});
|
||||
|
||||
it("requires a positive sequence", () => {
|
||||
expect(schema().safeParse({
|
||||
...base,
|
||||
sequence: 0,
|
||||
type: "session.started",
|
||||
payload: { cwd: "workspace/sessions/ses_1" },
|
||||
}).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as shared from "../src/index.js";
|
||||
|
||||
function messageSchema(): { safeParse(value: unknown): { success: boolean } } {
|
||||
const candidate = Reflect.get(shared, "MessageInputSchema");
|
||||
expect(candidate, "MessageInputSchema must be exported").toBeDefined();
|
||||
return candidate;
|
||||
}
|
||||
|
||||
describe("MessageInputSchema", () => {
|
||||
it("accepts text-only, image-only, and combined messages", () => {
|
||||
expect(messageSchema().safeParse({ text: "hello", attachmentIds: [] }).success).toBe(true);
|
||||
expect(messageSchema().safeParse({ text: "", attachmentIds: ["att_123"] }).success).toBe(true);
|
||||
expect(messageSchema().safeParse({ text: "look", attachmentIds: ["att_123"] }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a message without text or attachments", () => {
|
||||
expect(messageSchema().safeParse({ text: " ", attachmentIds: [] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects duplicate or malformed attachment ids", () => {
|
||||
expect(messageSchema().safeParse({ text: "x", attachmentIds: ["att_1", "att_1"] }).success).toBe(false);
|
||||
expect(messageSchema().safeParse({ text: "x", attachmentIds: ["../secret"] }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as shared from "../src/index.js";
|
||||
|
||||
function modelSchema(): { safeParse(value: unknown): { success: boolean } } {
|
||||
const candidate = Reflect.get(shared, "ModelConnectionInputSchema");
|
||||
expect(candidate, "ModelConnectionInputSchema must be exported").toBeDefined();
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const valid = {
|
||||
providerId: "openai",
|
||||
api: "openai-responses",
|
||||
modelId: "gpt-image",
|
||||
supportsImages: true,
|
||||
};
|
||||
|
||||
describe("ModelConnectionInputSchema", () => {
|
||||
it("accepts a supported API with bounded model metadata", () => {
|
||||
expect(modelSchema().safeParse({
|
||||
...valid,
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
apiKey: "sk-example",
|
||||
displayName: "Training model",
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 4096,
|
||||
}).success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unknown fields instead of silently stripping them", () => {
|
||||
expect(modelSchema().safeParse({ ...valid, headers: { authorization: "secret" } }).success).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["gemini", "openai-chat", ""])("rejects unsupported API %s", (api) => {
|
||||
expect(modelSchema().safeParse({ ...valid, api }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user