* feat(hub): deploy canonical workflow bundles Keep CLI deployment aligned with Hub's authored bundle boundary so validation and activation receive the same deterministic source files. Public documentation now teaches that boundary as the only configuration format. * test(hub): assert validation diagnostics directly * refactor(hub): deepen HTTP client module * feat(hub): validate named agent configurations * docs(hub): align Claude mode example
393 lines
12 KiB
TypeScript
393 lines
12 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import {
|
|
HubMessageCorrelationError,
|
|
SessionInboundMessageSchema,
|
|
SessionOutboundMessageSchema,
|
|
parseHubExecutionOutboundMessage,
|
|
} from "./messages.js";
|
|
|
|
const agent = {
|
|
id: "agent-1",
|
|
provider: "codex",
|
|
cwd: "/workspace",
|
|
model: null,
|
|
createdAt: "2026-07-13T00:00:00.000Z",
|
|
updatedAt: "2026-07-13T00:00:00.000Z",
|
|
lastUserMessageAt: null,
|
|
status: "idle",
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: false,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
supportsRewindConversation: false,
|
|
supportsRewindFiles: false,
|
|
supportsRewindBoth: false,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
title: null,
|
|
labels: {},
|
|
};
|
|
|
|
// Frozen at the Hub create request shape shipped before worktree.
|
|
const PreviousHubAgentCreateRequestSchema = z.object({
|
|
type: z.literal("hub.execution.agent.create.request"),
|
|
requestId: z.string(),
|
|
executionId: z.string(),
|
|
provider: z.string(),
|
|
cwd: z.string(),
|
|
workspaceId: z.string().optional(),
|
|
prompt: z.string(),
|
|
model: z.string().optional(),
|
|
modeId: z.string().optional(),
|
|
thinkingOptionId: z.string().optional(),
|
|
featureValues: z.record(z.string(), z.unknown()).optional(),
|
|
env: z.record(z.string(), z.string()).optional(),
|
|
});
|
|
|
|
// Frozen at the Hub create request shape that temporarily carried turn-based auto-archive policy.
|
|
const PreviousHubAgentCreateWithAutoArchiveRequestSchema =
|
|
PreviousHubAgentCreateRequestSchema.extend({
|
|
worktree: z
|
|
.object({
|
|
mode: z.literal("branch-off"),
|
|
newBranch: z.string(),
|
|
base: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
autoArchive: z.boolean().optional(),
|
|
});
|
|
|
|
describe("Hub session protocol", () => {
|
|
test("round-trips named-agent validation", () => {
|
|
const request = {
|
|
type: "hub.execution.agent.validate.request" as const,
|
|
requestId: "validate-codex",
|
|
provider: "codex",
|
|
model: "gpt-5.5",
|
|
thinkingOptionId: "xhigh",
|
|
providerOptions: {
|
|
sandbox_workspace_write: {
|
|
writable_roots: ["/var/cache/npm"],
|
|
network_access: false,
|
|
},
|
|
},
|
|
};
|
|
const response = {
|
|
type: "hub.execution.agent.validate.response" as const,
|
|
payload: {
|
|
requestId: request.requestId,
|
|
valid: true,
|
|
issues: [],
|
|
error: null,
|
|
},
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(request)).toEqual(request);
|
|
expect(SessionOutboundMessageSchema.parse(response)).toEqual(response);
|
|
});
|
|
|
|
test("accepts the Hub execution create request", () => {
|
|
const message = {
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "request-1",
|
|
executionId: "execution-1",
|
|
provider: "codex",
|
|
cwd: "/workspace",
|
|
prompt: "Implement the requested change",
|
|
modeId: "code",
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
|
});
|
|
|
|
test("keeps the retired Hub workspace selector wire-compatible", () => {
|
|
const message = {
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "request-retired-workspace",
|
|
executionId: "execution-retired-workspace",
|
|
provider: "codex",
|
|
cwd: "/workspace",
|
|
workspaceId: "caller-owned-workspace",
|
|
prompt: "Implement the requested change",
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
|
});
|
|
|
|
test("accepts an optional MCP server configuration on Hub creates", () => {
|
|
const message = {
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "request-mcp",
|
|
executionId: "execution-mcp",
|
|
provider: "codex",
|
|
cwd: "/workspace",
|
|
prompt: "Implement the requested change",
|
|
mcpServers: {
|
|
hub: {
|
|
type: "http",
|
|
url: "https://hub.example/mcp/executions/execution-mcp",
|
|
headers: { Authorization: "Bearer hub-execution-bearer" },
|
|
},
|
|
},
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
|
expect(PreviousHubAgentCreateRequestSchema.parse(message)).toEqual({
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "request-mcp",
|
|
executionId: "execution-mcp",
|
|
provider: "codex",
|
|
cwd: "/workspace",
|
|
prompt: "Implement the requested change",
|
|
});
|
|
});
|
|
|
|
test("round-trips native provider options and structured MCP preapproval", () => {
|
|
const message = {
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "request-policy",
|
|
executionId: "execution-policy",
|
|
provider: "codex",
|
|
cwd: "/workspace",
|
|
prompt: "Classify and finish",
|
|
providerOptions: {
|
|
sandbox_mode: "workspace-write",
|
|
sandbox_workspace_write: { writable_roots: ["/var/cache/npm"] },
|
|
},
|
|
mcpServers: {
|
|
hub: { type: "http", url: "https://hub.example/executions/policy" },
|
|
},
|
|
toolPolicy: {
|
|
preapproved: [{ kind: "mcp", server: "hub", tool: "finish_execution" }],
|
|
},
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
|
});
|
|
|
|
test.each(["Bash", "Edit", "Write"])("cannot encode native %s tool preapproval", (tool) => {
|
|
const message = {
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "request-native-tool",
|
|
executionId: "execution-native-tool",
|
|
provider: "claude",
|
|
cwd: "/workspace",
|
|
prompt: "Do work",
|
|
toolPolicy: { preapproved: [{ kind: "native", server: "claude", tool }] },
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.safeParse(message).success).toBe(false);
|
|
});
|
|
|
|
test.each([
|
|
undefined,
|
|
{ mode: "branch-off", newBranch: "hub-work", base: "main" },
|
|
{ mode: "checkout-branch", branch: "existing-work" },
|
|
{ mode: "checkout-pr", prNumber: 42 },
|
|
])("accepts Hub create worktree target %#", (worktree) => {
|
|
const message = {
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "hub-worktree",
|
|
executionId: "execution-worktree",
|
|
provider: "codex",
|
|
cwd: "/repo",
|
|
prompt: "Work in the requested target",
|
|
...(worktree ? { worktree } : {}),
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
|
});
|
|
|
|
test("accepts old Hub creates while ignoring their removed auto-archive policy", () => {
|
|
const oldRequest = {
|
|
type: "hub.execution.agent.create.request" as const,
|
|
requestId: "hub-old-policy",
|
|
executionId: "execution-old-policy",
|
|
provider: "codex",
|
|
cwd: "/repo",
|
|
prompt: "Work in the requested target",
|
|
worktree: { mode: "branch-off" as const, newBranch: "hub-work", base: "main" },
|
|
autoArchive: true,
|
|
};
|
|
|
|
expect(PreviousHubAgentCreateWithAutoArchiveRequestSchema.parse(oldRequest)).toEqual(
|
|
oldRequest,
|
|
);
|
|
expect(SessionInboundMessageSchema.parse(oldRequest)).toEqual({
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "hub-old-policy",
|
|
executionId: "execution-old-policy",
|
|
provider: "codex",
|
|
cwd: "/repo",
|
|
prompt: "Work in the requested target",
|
|
worktree: { mode: "branch-off", newBranch: "hub-work", base: "main" },
|
|
});
|
|
});
|
|
|
|
test.each(["interrupt", "archive"] as const)(
|
|
"round-trips the Hub execution %s command",
|
|
(action) => {
|
|
const request = {
|
|
type: "hub.execution.control.request" as const,
|
|
requestId: `control-${action}`,
|
|
executionId: "execution-1",
|
|
action,
|
|
};
|
|
const response = {
|
|
type: "hub.execution.control.response" as const,
|
|
payload: {
|
|
requestId: request.requestId,
|
|
executionId: request.executionId,
|
|
action,
|
|
success: true,
|
|
error: null,
|
|
},
|
|
};
|
|
|
|
expect(SessionInboundMessageSchema.parse(request)).toEqual(request);
|
|
expect(SessionOutboundMessageSchema.parse(response)).toEqual(response);
|
|
expect(parseHubExecutionOutboundMessage(response)).toEqual(response);
|
|
},
|
|
);
|
|
|
|
test("the previous Hub create parser ignores additive worktree and auto-archive fields", () => {
|
|
const newRequest = {
|
|
type: "hub.execution.agent.create.request" as const,
|
|
requestId: "hub-worktree",
|
|
executionId: "execution-worktree",
|
|
provider: "codex",
|
|
cwd: "/repo",
|
|
prompt: "Work in the requested target",
|
|
worktree: { mode: "branch-off", newBranch: "hub-work", base: "main" },
|
|
autoArchive: true,
|
|
};
|
|
|
|
expect(PreviousHubAgentCreateRequestSchema.parse(newRequest)).toEqual({
|
|
type: "hub.execution.agent.create.request",
|
|
requestId: "hub-worktree",
|
|
executionId: "execution-worktree",
|
|
provider: "codex",
|
|
cwd: "/repo",
|
|
prompt: "Work in the requested target",
|
|
});
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
type: "hub.execution.agent.create.response",
|
|
payload: {
|
|
requestId: "request-1",
|
|
executionId: "execution-1",
|
|
agentId: "agent-1",
|
|
agent,
|
|
success: true,
|
|
error: null,
|
|
},
|
|
},
|
|
{
|
|
type: "hub.execution.control.response",
|
|
payload: {
|
|
requestId: "control-1",
|
|
executionId: "execution-1",
|
|
action: "archive",
|
|
success: false,
|
|
error: "Execution not found",
|
|
},
|
|
},
|
|
{
|
|
type: "hub.execution.agent.update",
|
|
payload: { executionId: "execution-1", agentId: "agent-1", agent },
|
|
},
|
|
{
|
|
type: "hub.execution.agent.stream",
|
|
payload: {
|
|
executionId: "execution-1",
|
|
agentId: "agent-1",
|
|
event: { type: "turn_started", provider: "codex" },
|
|
},
|
|
},
|
|
])("accepts outbound variant $type", (message) => {
|
|
expect(SessionOutboundMessageSchema.parse(message)).toEqual(message);
|
|
expect(parseHubExecutionOutboundMessage(message)).toEqual(message);
|
|
});
|
|
|
|
test("rejects a Hub update whose correlated agent ids disagree", () => {
|
|
const malformed = {
|
|
type: "hub.execution.agent.update",
|
|
payload: { executionId: "execution-1", agentId: "agent-2", agent },
|
|
};
|
|
|
|
expect(SessionOutboundMessageSchema.safeParse(malformed).success).toBe(true);
|
|
expect(() => parseHubExecutionOutboundMessage(malformed)).toThrow(HubMessageCorrelationError);
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
type: "hub.management.daemon.connect.request",
|
|
requestId: "r1",
|
|
hubUrl: "https://hub.example",
|
|
token: "token",
|
|
},
|
|
{ type: "hub.management.daemon.get_status.request", requestId: "r2" },
|
|
{ type: "hub.management.daemon.disconnect.request", requestId: "r3", force: true },
|
|
])("accepts trusted management request $type", (message) => {
|
|
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
|
});
|
|
|
|
test.each([
|
|
{
|
|
type: "hub.management.daemon.connect.response",
|
|
payload: {
|
|
requestId: "r1",
|
|
status: {
|
|
state: "connected",
|
|
daemonId: "daemon-1",
|
|
hubOrigin: "https://hub.example",
|
|
scopes: ["hub.execution.*"],
|
|
connectedAt: "2026-07-13T00:00:00.000Z",
|
|
lastError: null,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "hub.management.daemon.get_status.response",
|
|
payload: {
|
|
requestId: "r2",
|
|
status: {
|
|
state: "not_connected",
|
|
daemonId: null,
|
|
hubOrigin: null,
|
|
scopes: [],
|
|
connectedAt: null,
|
|
lastError: null,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "hub.management.daemon.disconnect.response",
|
|
payload: {
|
|
requestId: "r3",
|
|
status: {
|
|
state: "disconnecting",
|
|
daemonId: "daemon-1",
|
|
hubOrigin: "https://hub.example",
|
|
scopes: ["hub.execution.*"],
|
|
connectedAt: null,
|
|
lastError: "offline",
|
|
},
|
|
warning: "pending",
|
|
},
|
|
},
|
|
])("accepts trusted management response $type", (message) => {
|
|
expect(SessionOutboundMessageSchema.parse(message)).toEqual(message);
|
|
});
|
|
});
|