mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 07:04:48 +00:00
feat: sync universal schema and sdk updates
This commit is contained in:
parent
79bb441287
commit
f5d1a6383d
56 changed files with 6800 additions and 3974 deletions
|
|
@ -11,6 +11,12 @@
|
|||
"sandbox-agent": "bin/sandbox-agent",
|
||||
"sandbox-daemon": "bin/sandbox-agent"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sandbox-agent/cli-darwin-arm64": "0.1.0",
|
||||
"@sandbox-agent/cli-darwin-x64": "0.1.0",
|
||||
|
|
|
|||
97
sdks/cli/tests/launcher.test.ts
Normal file
97
sdks/cli/tests/launcher.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const LAUNCHER_PATH = resolve(__dirname, "../bin/sandbox-agent");
|
||||
|
||||
// Check for binary in common locations
|
||||
function findBinary(): string | null {
|
||||
if (process.env.SANDBOX_AGENT_BIN) {
|
||||
return process.env.SANDBOX_AGENT_BIN;
|
||||
}
|
||||
|
||||
// Check cargo build output
|
||||
const cargoPaths = [
|
||||
resolve(__dirname, "../../../target/debug/sandbox-agent"),
|
||||
resolve(__dirname, "../../../target/release/sandbox-agent"),
|
||||
];
|
||||
|
||||
for (const p of cargoPaths) {
|
||||
if (existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const BINARY_PATH = findBinary();
|
||||
const SKIP_INTEGRATION = !BINARY_PATH;
|
||||
|
||||
describe("CLI Launcher", () => {
|
||||
describe("platform detection", () => {
|
||||
it("defines all supported platforms", () => {
|
||||
const PLATFORMS: Record<string, string> = {
|
||||
"darwin-arm64": "@sandbox-agent/cli-darwin-arm64",
|
||||
"darwin-x64": "@sandbox-agent/cli-darwin-x64",
|
||||
"linux-x64": "@sandbox-agent/cli-linux-x64",
|
||||
"win32-x64": "@sandbox-agent/cli-win32-x64",
|
||||
};
|
||||
|
||||
// Verify platform map covers expected platforms
|
||||
expect(PLATFORMS["darwin-arm64"]).toBe("@sandbox-agent/cli-darwin-arm64");
|
||||
expect(PLATFORMS["darwin-x64"]).toBe("@sandbox-agent/cli-darwin-x64");
|
||||
expect(PLATFORMS["linux-x64"]).toBe("@sandbox-agent/cli-linux-x64");
|
||||
expect(PLATFORMS["win32-x64"]).toBe("@sandbox-agent/cli-win32-x64");
|
||||
});
|
||||
|
||||
it("generates correct platform key format", () => {
|
||||
const key = `${process.platform}-${process.arch}`;
|
||||
expect(key).toMatch(/^[a-z0-9]+-[a-z0-9]+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(SKIP_INTEGRATION)("CLI Integration", () => {
|
||||
it("runs --help successfully", () => {
|
||||
const result = spawnSync(BINARY_PATH!, ["--help"], {
|
||||
encoding: "utf8",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("sandbox-agent");
|
||||
});
|
||||
|
||||
it("runs --version successfully", () => {
|
||||
const result = spawnSync(BINARY_PATH!, ["--version"], {
|
||||
encoding: "utf8",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toMatch(/\d+\.\d+\.\d+/);
|
||||
});
|
||||
|
||||
it("lists agents", () => {
|
||||
const result = spawnSync(BINARY_PATH!, ["agents", "list"], {
|
||||
encoding: "utf8",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
it("shows server help", () => {
|
||||
const result = spawnSync(BINARY_PATH!, ["server", "--help"], {
|
||||
encoding: "utf8",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("server");
|
||||
});
|
||||
});
|
||||
8
sdks/cli/vitest.config.ts
Normal file
8
sdks/cli/vitest.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["tests/**/*.test.ts"],
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
|
|
@ -23,13 +23,17 @@
|
|||
"generate:openapi": "cargo check -p sandbox-agent-openapi-gen && cargo run -p sandbox-agent-openapi-gen -- --out ../../docs/openapi.json",
|
||||
"generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/openapi.ts",
|
||||
"generate": "pnpm run generate:openapi && pnpm run generate:types",
|
||||
"build": "pnpm run generate && tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"build": "pnpm run generate && tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"openapi-typescript": "^6.7.0",
|
||||
"typescript": "^5.7.0"
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sandbox-agent/cli": "0.1.0"
|
||||
|
|
|
|||
|
|
@ -1,31 +1,23 @@
|
|||
import type { components } from "./generated/openapi.js";
|
||||
import type {
|
||||
SandboxDaemonSpawnHandle,
|
||||
SandboxDaemonSpawnOptions,
|
||||
} from "./spawn.js";
|
||||
|
||||
export type AgentInstallRequest = components["schemas"]["AgentInstallRequest"];
|
||||
export type AgentModeInfo = components["schemas"]["AgentModeInfo"];
|
||||
export type AgentModesResponse = components["schemas"]["AgentModesResponse"];
|
||||
export type AgentInfo = components["schemas"]["AgentInfo"];
|
||||
export type AgentListResponse = components["schemas"]["AgentListResponse"];
|
||||
export type CreateSessionRequest = components["schemas"]["CreateSessionRequest"];
|
||||
export type CreateSessionResponse = components["schemas"]["CreateSessionResponse"];
|
||||
export type HealthResponse = components["schemas"]["HealthResponse"];
|
||||
export type MessageRequest = components["schemas"]["MessageRequest"];
|
||||
export type EventsQuery = components["schemas"]["EventsQuery"];
|
||||
export type EventsResponse = components["schemas"]["EventsResponse"];
|
||||
export type PermissionRequest = components["schemas"]["PermissionRequest"];
|
||||
export type QuestionReplyRequest = components["schemas"]["QuestionReplyRequest"];
|
||||
export type QuestionRequest = components["schemas"]["QuestionRequest"];
|
||||
export type PermissionReplyRequest = components["schemas"]["PermissionReplyRequest"];
|
||||
export type PermissionReply = components["schemas"]["PermissionReply"];
|
||||
export type ProblemDetails = components["schemas"]["ProblemDetails"];
|
||||
export type SessionInfo = components["schemas"]["SessionInfo"];
|
||||
export type SessionListResponse = components["schemas"]["SessionListResponse"];
|
||||
export type UniversalEvent = components["schemas"]["UniversalEvent"];
|
||||
export type UniversalMessage = components["schemas"]["UniversalMessage"];
|
||||
export type UniversalMessagePart = components["schemas"]["UniversalMessagePart"];
|
||||
} from "./spawn.ts";
|
||||
import type {
|
||||
AgentInstallRequest,
|
||||
AgentListResponse,
|
||||
AgentModesResponse,
|
||||
CreateSessionRequest,
|
||||
CreateSessionResponse,
|
||||
EventsQuery,
|
||||
EventsResponse,
|
||||
HealthResponse,
|
||||
MessageRequest,
|
||||
PermissionReplyRequest,
|
||||
ProblemDetails,
|
||||
QuestionReplyRequest,
|
||||
SessionListResponse,
|
||||
UniversalEvent,
|
||||
} from "./types.ts";
|
||||
|
||||
const API_PREFIX = "/v1";
|
||||
|
||||
|
|
@ -179,13 +171,14 @@ export class SandboxDaemonClient {
|
|||
if (done) {
|
||||
break;
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
// Normalize CRLF to LF for consistent parsing
|
||||
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
||||
let index = buffer.indexOf("\n\n");
|
||||
while (index !== -1) {
|
||||
const chunk = buffer.slice(0, index);
|
||||
buffer = buffer.slice(index + 2);
|
||||
const dataLines = chunk
|
||||
.split(/\r?\n/)
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data:"));
|
||||
if (dataLines.length > 0) {
|
||||
const payload = dataLines
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@
|
|||
*/
|
||||
|
||||
|
||||
/** OneOf type helpers */
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||
type OneOf<T extends any[]> = T extends [infer Only] ? Only : T extends [infer A, infer B, ...infer Rest] ? OneOf<[XOR<A, B>, ...Rest]> : never;
|
||||
|
||||
export interface paths {
|
||||
"/v1/agents": {
|
||||
get: operations["list_agents"];
|
||||
|
|
@ -46,12 +41,21 @@ export interface paths {
|
|||
"/v1/sessions/{session_id}/questions/{question_id}/reply": {
|
||||
post: operations["reply_question"];
|
||||
};
|
||||
"/v1/sessions/{session_id}/terminate": {
|
||||
post: operations["terminate_session"];
|
||||
};
|
||||
}
|
||||
|
||||
export type webhooks = Record<string, never>;
|
||||
|
||||
export interface components {
|
||||
schemas: {
|
||||
AgentCapabilities: {
|
||||
permissions: boolean;
|
||||
planMode: boolean;
|
||||
questions: boolean;
|
||||
toolCalls: boolean;
|
||||
};
|
||||
AgentError: {
|
||||
agent?: string | null;
|
||||
details?: unknown;
|
||||
|
|
@ -60,6 +64,7 @@ export interface components {
|
|||
type: components["schemas"]["ErrorType"];
|
||||
};
|
||||
AgentInfo: {
|
||||
capabilities: components["schemas"]["AgentCapabilities"];
|
||||
id: string;
|
||||
installed: boolean;
|
||||
path?: string | null;
|
||||
|
|
@ -79,25 +84,52 @@ export interface components {
|
|||
AgentModesResponse: {
|
||||
modes: components["schemas"]["AgentModeInfo"][];
|
||||
};
|
||||
AttachmentSource: {
|
||||
AgentUnparsedData: {
|
||||
error: string;
|
||||
location: string;
|
||||
raw_hash?: string | null;
|
||||
};
|
||||
ContentPart: {
|
||||
text: string;
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
} | {
|
||||
json: unknown;
|
||||
/** @enum {string} */
|
||||
type: "json";
|
||||
} | {
|
||||
arguments: string;
|
||||
call_id: string;
|
||||
name: string;
|
||||
/** @enum {string} */
|
||||
type: "tool_call";
|
||||
} | {
|
||||
call_id: string;
|
||||
output: string;
|
||||
/** @enum {string} */
|
||||
type: "tool_result";
|
||||
} | ({
|
||||
action: components["schemas"]["FileAction"];
|
||||
diff?: string | null;
|
||||
path: string;
|
||||
/** @enum {string} */
|
||||
type: "path";
|
||||
} | {
|
||||
type: "file_ref";
|
||||
}) | {
|
||||
text: string;
|
||||
/** @enum {string} */
|
||||
type: "url";
|
||||
url: string;
|
||||
type: "reasoning";
|
||||
visibility: components["schemas"]["ReasoningVisibility"];
|
||||
} | ({
|
||||
data: string;
|
||||
encoding?: string | null;
|
||||
mime?: string | null;
|
||||
path: string;
|
||||
/** @enum {string} */
|
||||
type: "data";
|
||||
type: "image";
|
||||
}) | ({
|
||||
detail?: string | null;
|
||||
label: string;
|
||||
/** @enum {string} */
|
||||
type: "status";
|
||||
});
|
||||
CrashInfo: {
|
||||
details?: unknown;
|
||||
kind?: string | null;
|
||||
message: string;
|
||||
};
|
||||
CreateSessionRequest: {
|
||||
agent: string;
|
||||
agentMode?: string | null;
|
||||
|
|
@ -107,13 +139,21 @@ export interface components {
|
|||
variant?: string | null;
|
||||
};
|
||||
CreateSessionResponse: {
|
||||
agentSessionId?: string | null;
|
||||
error?: components["schemas"]["AgentError"] | null;
|
||||
healthy: boolean;
|
||||
nativeSessionId?: string | null;
|
||||
};
|
||||
ErrorData: {
|
||||
code?: string | null;
|
||||
details?: unknown;
|
||||
message: string;
|
||||
};
|
||||
/** @enum {string} */
|
||||
ErrorType: "invalid_request" | "unsupported_agent" | "agent_not_installed" | "install_failed" | "agent_process_exited" | "token_invalid" | "permission_denied" | "session_not_found" | "session_already_exists" | "mode_not_supported" | "stream_error" | "timeout";
|
||||
/** @enum {string} */
|
||||
EventSource: "agent" | "daemon";
|
||||
EventsQuery: {
|
||||
includeRaw?: boolean | null;
|
||||
/** Format: int64 */
|
||||
limit?: number | null;
|
||||
/** Format: int64 */
|
||||
|
|
@ -123,32 +163,41 @@ export interface components {
|
|||
events: components["schemas"]["UniversalEvent"][];
|
||||
hasMore: boolean;
|
||||
};
|
||||
/** @enum {string} */
|
||||
FileAction: "read" | "write" | "patch";
|
||||
HealthResponse: {
|
||||
status: string;
|
||||
};
|
||||
ItemDeltaData: {
|
||||
delta: string;
|
||||
item_id: string;
|
||||
native_item_id?: string | null;
|
||||
};
|
||||
ItemEventData: {
|
||||
item: components["schemas"]["UniversalItem"];
|
||||
};
|
||||
/** @enum {string} */
|
||||
ItemKind: "message" | "tool_call" | "tool_result" | "system" | "status" | "unknown";
|
||||
/** @enum {string} */
|
||||
ItemRole: "user" | "assistant" | "system" | "tool";
|
||||
/** @enum {string} */
|
||||
ItemStatus: "in_progress" | "completed" | "failed";
|
||||
MessageRequest: {
|
||||
message: string;
|
||||
};
|
||||
PermissionEventData: {
|
||||
action: string;
|
||||
metadata?: unknown;
|
||||
permission_id: string;
|
||||
status: components["schemas"]["PermissionStatus"];
|
||||
};
|
||||
/** @enum {string} */
|
||||
PermissionReply: "once" | "always" | "reject";
|
||||
PermissionReplyRequest: {
|
||||
reply: components["schemas"]["PermissionReply"];
|
||||
};
|
||||
PermissionRequest: {
|
||||
always: string[];
|
||||
id: string;
|
||||
metadata?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
patterns: string[];
|
||||
permission: string;
|
||||
sessionId: string;
|
||||
tool?: components["schemas"]["PermissionToolRef"] | null;
|
||||
};
|
||||
PermissionToolRef: {
|
||||
callId: string;
|
||||
messageId: string;
|
||||
};
|
||||
/** @enum {string} */
|
||||
PermissionStatus: "requested" | "approved" | "denied";
|
||||
ProblemDetails: {
|
||||
detail?: string | null;
|
||||
instance?: string | null;
|
||||
|
|
@ -158,38 +207,34 @@ export interface components {
|
|||
type: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
QuestionInfo: {
|
||||
custom?: boolean | null;
|
||||
header?: string | null;
|
||||
multiSelect?: boolean | null;
|
||||
options: components["schemas"]["QuestionOption"][];
|
||||
question: string;
|
||||
};
|
||||
QuestionOption: {
|
||||
description?: string | null;
|
||||
label: string;
|
||||
QuestionEventData: {
|
||||
options: string[];
|
||||
prompt: string;
|
||||
question_id: string;
|
||||
response?: string | null;
|
||||
status: components["schemas"]["QuestionStatus"];
|
||||
};
|
||||
QuestionReplyRequest: {
|
||||
answers: string[][];
|
||||
};
|
||||
QuestionRequest: {
|
||||
id: string;
|
||||
questions: components["schemas"]["QuestionInfo"][];
|
||||
sessionId: string;
|
||||
tool?: components["schemas"]["QuestionToolRef"] | null;
|
||||
};
|
||||
QuestionToolRef: {
|
||||
callId: string;
|
||||
messageId: string;
|
||||
/** @enum {string} */
|
||||
QuestionStatus: "requested" | "answered" | "rejected";
|
||||
/** @enum {string} */
|
||||
ReasoningVisibility: "public" | "private";
|
||||
/** @enum {string} */
|
||||
SessionEndReason: "completed" | "error" | "terminated";
|
||||
SessionEndedData: {
|
||||
reason: components["schemas"]["SessionEndReason"];
|
||||
terminated_by: components["schemas"]["TerminatedBy"];
|
||||
};
|
||||
SessionInfo: {
|
||||
agent: string;
|
||||
agentMode: string;
|
||||
agentSessionId?: string | null;
|
||||
ended: boolean;
|
||||
/** Format: int64 */
|
||||
eventCount: number;
|
||||
model?: string | null;
|
||||
nativeSessionId?: string | null;
|
||||
permissionMode: string;
|
||||
sessionId: string;
|
||||
variant?: string | null;
|
||||
|
|
@ -197,98 +242,35 @@ export interface components {
|
|||
SessionListResponse: {
|
||||
sessions: components["schemas"]["SessionInfo"][];
|
||||
};
|
||||
Started: {
|
||||
details?: unknown;
|
||||
message?: string | null;
|
||||
SessionStartedData: {
|
||||
metadata?: unknown;
|
||||
};
|
||||
/** @enum {string} */
|
||||
TerminatedBy: "agent" | "daemon";
|
||||
UniversalEvent: {
|
||||
agent: string;
|
||||
agentSessionId?: string | null;
|
||||
data: components["schemas"]["UniversalEventData"];
|
||||
event_id: string;
|
||||
native_session_id?: string | null;
|
||||
raw?: unknown;
|
||||
/** Format: int64 */
|
||||
id: number;
|
||||
sessionId: string;
|
||||
timestamp: string;
|
||||
sequence: number;
|
||||
session_id: string;
|
||||
source: components["schemas"]["EventSource"];
|
||||
synthetic: boolean;
|
||||
time: string;
|
||||
type: components["schemas"]["UniversalEventType"];
|
||||
};
|
||||
UniversalEventData: {
|
||||
message: components["schemas"]["UniversalMessage"];
|
||||
} | {
|
||||
started: components["schemas"]["Started"];
|
||||
} | {
|
||||
error: components["schemas"]["CrashInfo"];
|
||||
} | {
|
||||
questionAsked: components["schemas"]["QuestionRequest"];
|
||||
} | {
|
||||
permissionAsked: components["schemas"]["PermissionRequest"];
|
||||
} | {
|
||||
raw: unknown;
|
||||
};
|
||||
UniversalMessage: OneOf<[components["schemas"]["UniversalMessageParsed"], {
|
||||
error?: string | null;
|
||||
raw: unknown;
|
||||
}]>;
|
||||
UniversalMessageParsed: {
|
||||
id?: string | null;
|
||||
metadata?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
parts: components["schemas"]["UniversalMessagePart"][];
|
||||
role: string;
|
||||
};
|
||||
UniversalMessagePart: {
|
||||
text: string;
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
} | ({
|
||||
id?: string | null;
|
||||
input: unknown;
|
||||
name: string;
|
||||
/** @enum {string} */
|
||||
type: "tool_call";
|
||||
}) | ({
|
||||
id?: string | null;
|
||||
is_error?: boolean | null;
|
||||
name?: string | null;
|
||||
output: unknown;
|
||||
/** @enum {string} */
|
||||
type: "tool_result";
|
||||
}) | ({
|
||||
arguments: unknown;
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
raw?: unknown;
|
||||
/** @enum {string} */
|
||||
type: "function_call";
|
||||
}) | ({
|
||||
id?: string | null;
|
||||
is_error?: boolean | null;
|
||||
name?: string | null;
|
||||
raw?: unknown;
|
||||
result: unknown;
|
||||
/** @enum {string} */
|
||||
type: "function_result";
|
||||
}) | ({
|
||||
filename?: string | null;
|
||||
mime_type?: string | null;
|
||||
raw?: unknown;
|
||||
source: components["schemas"]["AttachmentSource"];
|
||||
/** @enum {string} */
|
||||
type: "file";
|
||||
}) | ({
|
||||
alt?: string | null;
|
||||
mime_type?: string | null;
|
||||
raw?: unknown;
|
||||
source: components["schemas"]["AttachmentSource"];
|
||||
/** @enum {string} */
|
||||
type: "image";
|
||||
}) | {
|
||||
message: string;
|
||||
/** @enum {string} */
|
||||
type: "error";
|
||||
} | {
|
||||
raw: unknown;
|
||||
/** @enum {string} */
|
||||
type: "unknown";
|
||||
UniversalEventData: components["schemas"]["SessionStartedData"] | components["schemas"]["SessionEndedData"] | components["schemas"]["ItemEventData"] | components["schemas"]["ItemDeltaData"] | components["schemas"]["ErrorData"] | components["schemas"]["PermissionEventData"] | components["schemas"]["QuestionEventData"] | components["schemas"]["AgentUnparsedData"];
|
||||
/** @enum {string} */
|
||||
UniversalEventType: "session.started" | "session.ended" | "item.started" | "item.delta" | "item.completed" | "error" | "permission.requested" | "permission.resolved" | "question.requested" | "question.resolved" | "agent.unparsed";
|
||||
UniversalItem: {
|
||||
content: components["schemas"]["ContentPart"][];
|
||||
item_id: string;
|
||||
kind: components["schemas"]["ItemKind"];
|
||||
native_item_id?: string | null;
|
||||
parent_id?: string | null;
|
||||
role?: components["schemas"]["ItemRole"] | null;
|
||||
status: components["schemas"]["ItemStatus"];
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
|
|
@ -418,10 +400,12 @@ export interface operations {
|
|||
get_events: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Last seen event id (exclusive) */
|
||||
/** @description Last seen event sequence (exclusive) */
|
||||
offset?: number | null;
|
||||
/** @description Max events to return */
|
||||
limit?: number | null;
|
||||
/** @description Include raw provider payloads */
|
||||
include_raw?: boolean | null;
|
||||
};
|
||||
path: {
|
||||
/** @description Session id */
|
||||
|
|
@ -444,8 +428,10 @@ export interface operations {
|
|||
get_events_sse: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Last seen event id (exclusive) */
|
||||
/** @description Last seen event sequence (exclusive) */
|
||||
offset?: number | null;
|
||||
/** @description Include raw provider payloads */
|
||||
include_raw?: boolean | null;
|
||||
};
|
||||
path: {
|
||||
/** @description Session id */
|
||||
|
|
@ -556,4 +542,23 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
terminate_session: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Session id */
|
||||
session_id: string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Session terminated */
|
||||
204: {
|
||||
content: never;
|
||||
};
|
||||
404: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,32 +3,53 @@ export {
|
|||
SandboxDaemonError,
|
||||
connectSandboxDaemonClient,
|
||||
createSandboxDaemonClient,
|
||||
} from "./client.js";
|
||||
} from "./client.ts";
|
||||
export type {
|
||||
SandboxDaemonClientOptions,
|
||||
SandboxDaemonConnectOptions,
|
||||
} from "./client.ts";
|
||||
export type {
|
||||
AgentCapabilities,
|
||||
AgentInfo,
|
||||
AgentInstallRequest,
|
||||
AgentListResponse,
|
||||
AgentModeInfo,
|
||||
AgentModesResponse,
|
||||
AgentUnparsedData,
|
||||
ContentPart,
|
||||
CreateSessionRequest,
|
||||
CreateSessionResponse,
|
||||
ErrorData,
|
||||
EventSource,
|
||||
EventsQuery,
|
||||
EventsResponse,
|
||||
FileAction,
|
||||
HealthResponse,
|
||||
ItemDeltaData,
|
||||
ItemEventData,
|
||||
ItemKind,
|
||||
ItemRole,
|
||||
ItemStatus,
|
||||
MessageRequest,
|
||||
PermissionRequest,
|
||||
PermissionEventData,
|
||||
PermissionReply,
|
||||
PermissionReplyRequest,
|
||||
PermissionStatus,
|
||||
ProblemDetails,
|
||||
QuestionRequest,
|
||||
QuestionEventData,
|
||||
QuestionReplyRequest,
|
||||
QuestionStatus,
|
||||
ReasoningVisibility,
|
||||
SessionEndReason,
|
||||
SessionEndedData,
|
||||
SessionInfo,
|
||||
SessionListResponse,
|
||||
SessionStartedData,
|
||||
TerminatedBy,
|
||||
UniversalEvent,
|
||||
UniversalMessage,
|
||||
UniversalMessagePart,
|
||||
SandboxDaemonClientOptions,
|
||||
SandboxDaemonConnectOptions,
|
||||
} from "./client.js";
|
||||
export type { components, paths } from "./generated/openapi.js";
|
||||
export type { SandboxDaemonSpawnOptions, SandboxDaemonSpawnLogMode } from "./spawn.js";
|
||||
UniversalEventData,
|
||||
UniversalEventType,
|
||||
UniversalItem,
|
||||
} from "./types.ts";
|
||||
export type { components, paths } from "./generated/openapi.ts";
|
||||
export type { SandboxDaemonSpawnOptions, SandboxDaemonSpawnLogMode } from "./spawn.ts";
|
||||
|
|
|
|||
45
sdks/typescript/src/types.ts
Normal file
45
sdks/typescript/src/types.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import type { components } from "./generated/openapi.ts";
|
||||
|
||||
type S = components["schemas"];
|
||||
|
||||
export type AgentCapabilities = S["AgentCapabilities"];
|
||||
export type AgentInfo = S["AgentInfo"];
|
||||
export type AgentInstallRequest = S["AgentInstallRequest"];
|
||||
export type AgentListResponse = S["AgentListResponse"];
|
||||
export type AgentModeInfo = S["AgentModeInfo"];
|
||||
export type AgentModesResponse = S["AgentModesResponse"];
|
||||
export type AgentUnparsedData = S["AgentUnparsedData"];
|
||||
export type ContentPart = S["ContentPart"];
|
||||
export type CreateSessionRequest = S["CreateSessionRequest"];
|
||||
export type CreateSessionResponse = S["CreateSessionResponse"];
|
||||
export type ErrorData = S["ErrorData"];
|
||||
export type EventSource = S["EventSource"];
|
||||
export type EventsQuery = S["EventsQuery"];
|
||||
export type EventsResponse = S["EventsResponse"];
|
||||
export type FileAction = S["FileAction"];
|
||||
export type HealthResponse = S["HealthResponse"];
|
||||
export type ItemDeltaData = S["ItemDeltaData"];
|
||||
export type ItemEventData = S["ItemEventData"];
|
||||
export type ItemKind = S["ItemKind"];
|
||||
export type ItemRole = S["ItemRole"];
|
||||
export type ItemStatus = S["ItemStatus"];
|
||||
export type MessageRequest = S["MessageRequest"];
|
||||
export type PermissionEventData = S["PermissionEventData"];
|
||||
export type PermissionReply = S["PermissionReply"];
|
||||
export type PermissionReplyRequest = S["PermissionReplyRequest"];
|
||||
export type PermissionStatus = S["PermissionStatus"];
|
||||
export type ProblemDetails = S["ProblemDetails"];
|
||||
export type QuestionEventData = S["QuestionEventData"];
|
||||
export type QuestionReplyRequest = S["QuestionReplyRequest"];
|
||||
export type QuestionStatus = S["QuestionStatus"];
|
||||
export type ReasoningVisibility = S["ReasoningVisibility"];
|
||||
export type SessionEndReason = S["SessionEndReason"];
|
||||
export type SessionEndedData = S["SessionEndedData"];
|
||||
export type SessionInfo = S["SessionInfo"];
|
||||
export type SessionListResponse = S["SessionListResponse"];
|
||||
export type SessionStartedData = S["SessionStartedData"];
|
||||
export type TerminatedBy = S["TerminatedBy"];
|
||||
export type UniversalEvent = S["UniversalEvent"];
|
||||
export type UniversalEventData = S["UniversalEventData"];
|
||||
export type UniversalEventType = S["UniversalEventType"];
|
||||
export type UniversalItem = S["UniversalItem"];
|
||||
305
sdks/typescript/tests/client.test.ts
Normal file
305
sdks/typescript/tests/client.test.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import { describe, it, expect, vi, type Mock } from "vitest";
|
||||
import { SandboxDaemonClient, SandboxDaemonError } from "../src/client.ts";
|
||||
|
||||
function createMockFetch(
|
||||
response: unknown,
|
||||
status = 200,
|
||||
headers: Record<string, string> = {}
|
||||
): Mock<typeof fetch> {
|
||||
return vi.fn<typeof fetch>().mockResolvedValue(
|
||||
new Response(JSON.stringify(response), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json", ...headers },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function createMockFetchError(status: number, problem: unknown): Mock<typeof fetch> {
|
||||
return vi.fn<typeof fetch>().mockResolvedValue(
|
||||
new Response(JSON.stringify(problem), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/problem+json" },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
describe("SandboxDaemonClient", () => {
|
||||
describe("constructor", () => {
|
||||
it("creates client with baseUrl", () => {
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
});
|
||||
expect(client).toBeInstanceOf(SandboxDaemonClient);
|
||||
});
|
||||
|
||||
it("strips trailing slash from baseUrl", async () => {
|
||||
const mockFetch = createMockFetch({ status: "ok" });
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080/",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.getHealth();
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/health",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if fetch is not available", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
// @ts-expect-error - testing missing fetch
|
||||
globalThis.fetch = undefined;
|
||||
|
||||
expect(() => {
|
||||
new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
});
|
||||
}).toThrow("Fetch API is not available");
|
||||
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
});
|
||||
|
||||
describe("connect", () => {
|
||||
it("creates client without spawn when baseUrl provided", async () => {
|
||||
const client = await SandboxDaemonClient.connect({
|
||||
baseUrl: "http://localhost:8080",
|
||||
spawn: false,
|
||||
});
|
||||
expect(client).toBeInstanceOf(SandboxDaemonClient);
|
||||
});
|
||||
|
||||
it("throws when no baseUrl and spawn disabled", async () => {
|
||||
await expect(
|
||||
SandboxDaemonClient.connect({ spawn: false })
|
||||
).rejects.toThrow("baseUrl is required when autospawn is disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHealth", () => {
|
||||
it("returns health response", async () => {
|
||||
const mockFetch = createMockFetch({ status: "ok" });
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const result = await client.getHealth();
|
||||
|
||||
expect(result).toEqual({ status: "ok" });
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/health",
|
||||
expect.objectContaining({ method: "GET" })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listAgents", () => {
|
||||
it("returns agent list", async () => {
|
||||
const agents = { agents: [{ id: "claude", installed: true }] };
|
||||
const mockFetch = createMockFetch(agents);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const result = await client.listAgents();
|
||||
|
||||
expect(result).toEqual(agents);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSession", () => {
|
||||
it("creates session with agent", async () => {
|
||||
const response = { healthy: true, agentSessionId: "abc123" };
|
||||
const mockFetch = createMockFetch(response);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const result = await client.createSession("test-session", {
|
||||
agent: "claude",
|
||||
});
|
||||
|
||||
expect(result).toEqual(response);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/sessions/test-session",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ agent: "claude" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("encodes session ID in URL", async () => {
|
||||
const mockFetch = createMockFetch({ healthy: true });
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.createSession("test/session", { agent: "claude" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/sessions/test%2Fsession",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("postMessage", () => {
|
||||
it("sends message to session", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue(
|
||||
new Response(null, { status: 204 })
|
||||
);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.postMessage("test-session", { message: "Hello" });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/sessions/test-session/messages",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ message: "Hello" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEvents", () => {
|
||||
it("returns events", async () => {
|
||||
const events = { events: [], hasMore: false };
|
||||
const mockFetch = createMockFetch(events);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const result = await client.getEvents("test-session");
|
||||
|
||||
expect(result).toEqual(events);
|
||||
});
|
||||
|
||||
it("passes query parameters", async () => {
|
||||
const mockFetch = createMockFetch({ events: [], hasMore: false });
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.getEvents("test-session", { offset: 10, limit: 50 });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/sessions/test-session/events?offset=10&limit=50",
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authentication", () => {
|
||||
it("includes authorization header when token provided", async () => {
|
||||
const mockFetch = createMockFetch({ status: "ok" });
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
token: "test-token",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.getHealth();
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.any(Headers),
|
||||
})
|
||||
);
|
||||
|
||||
const call = mockFetch.mock.calls[0];
|
||||
const headers = call?.[1]?.headers as Headers | undefined;
|
||||
expect(headers?.get("Authorization")).toBe("Bearer test-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("throws SandboxDaemonError on non-ok response", async () => {
|
||||
const problem = {
|
||||
type: "error",
|
||||
title: "Not Found",
|
||||
status: 404,
|
||||
detail: "Session not found",
|
||||
};
|
||||
const mockFetch = createMockFetchError(404, problem);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await expect(client.getEvents("nonexistent")).rejects.toThrow(
|
||||
SandboxDaemonError
|
||||
);
|
||||
|
||||
try {
|
||||
await client.getEvents("nonexistent");
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(SandboxDaemonError);
|
||||
const error = e as SandboxDaemonError;
|
||||
expect(error.status).toBe(404);
|
||||
expect(error.problem?.title).toBe("Not Found");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyQuestion", () => {
|
||||
it("sends question reply", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue(
|
||||
new Response(null, { status: 204 })
|
||||
);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.replyQuestion("test-session", "q1", {
|
||||
answers: [["Yes"]],
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/sessions/test-session/questions/q1/reply",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ answers: [["Yes"]] }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replyPermission", () => {
|
||||
it("sends permission reply", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue(
|
||||
new Response(null, { status: 204 })
|
||||
);
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
await client.replyPermission("test-session", "p1", {
|
||||
reply: "once",
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
"http://localhost:8080/v1/sessions/test-session/permissions/p1/reply",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ reply: "once" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
174
sdks/typescript/tests/integration.test.ts
Normal file
174
sdks/typescript/tests/integration.test.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { type ChildProcess } from "node:child_process";
|
||||
import { SandboxDaemonClient } from "../src/client.ts";
|
||||
import { spawnSandboxDaemon, isNodeRuntime } from "../src/spawn.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Check for binary in common locations
|
||||
function findBinary(): string | null {
|
||||
if (process.env.SANDBOX_AGENT_BIN) {
|
||||
return process.env.SANDBOX_AGENT_BIN;
|
||||
}
|
||||
|
||||
// Check cargo build output (run from sdks/typescript/tests)
|
||||
const cargoPaths = [
|
||||
resolve(__dirname, "../../../target/debug/sandbox-agent"),
|
||||
resolve(__dirname, "../../../target/release/sandbox-agent"),
|
||||
];
|
||||
|
||||
for (const p of cargoPaths) {
|
||||
if (existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const BINARY_PATH = findBinary();
|
||||
const SKIP_INTEGRATION = !BINARY_PATH && !process.env.RUN_INTEGRATION_TESTS;
|
||||
|
||||
// Set env var if we found a binary
|
||||
if (BINARY_PATH && !process.env.SANDBOX_AGENT_BIN) {
|
||||
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
|
||||
}
|
||||
|
||||
describe.skipIf(SKIP_INTEGRATION)("Integration: spawn (local mode)", () => {
|
||||
it("spawns daemon and connects", async () => {
|
||||
const handle = await spawnSandboxDaemon({
|
||||
enabled: true,
|
||||
log: "silent",
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
|
||||
try {
|
||||
expect(handle.baseUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
|
||||
expect(handle.token).toBeTruthy();
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: handle.baseUrl,
|
||||
token: handle.token,
|
||||
});
|
||||
|
||||
const health = await client.getHealth();
|
||||
expect(health.status).toBe("ok");
|
||||
} finally {
|
||||
await handle.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("SandboxDaemonClient.connect spawns automatically", async () => {
|
||||
const client = await SandboxDaemonClient.connect({
|
||||
spawn: { log: "silent", timeoutMs: 30000 },
|
||||
});
|
||||
|
||||
try {
|
||||
const health = await client.getHealth();
|
||||
expect(health.status).toBe("ok");
|
||||
|
||||
const agents = await client.listAgents();
|
||||
expect(agents.agents).toBeDefined();
|
||||
expect(Array.isArray(agents.agents)).toBe(true);
|
||||
} finally {
|
||||
await client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("lists available agents", async () => {
|
||||
const client = await SandboxDaemonClient.connect({
|
||||
spawn: { log: "silent", timeoutMs: 30000 },
|
||||
});
|
||||
|
||||
try {
|
||||
const agents = await client.listAgents();
|
||||
expect(agents.agents).toBeDefined();
|
||||
// Should have at least some agents defined
|
||||
expect(agents.agents.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
await client.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(SKIP_INTEGRATION)("Integration: connect (remote mode)", () => {
|
||||
let daemonProcess: ChildProcess;
|
||||
let baseUrl: string;
|
||||
let token: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Start daemon manually to simulate remote server
|
||||
const handle = await spawnSandboxDaemon({
|
||||
enabled: true,
|
||||
log: "silent",
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
daemonProcess = handle.child;
|
||||
baseUrl = handle.baseUrl;
|
||||
token = handle.token;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (daemonProcess && daemonProcess.exitCode === null) {
|
||||
daemonProcess.kill("SIGTERM");
|
||||
await new Promise<void>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
daemonProcess.kill("SIGKILL");
|
||||
resolve();
|
||||
}, 5000);
|
||||
daemonProcess.once("exit", () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("connects to remote server", async () => {
|
||||
const client = await SandboxDaemonClient.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
spawn: false,
|
||||
});
|
||||
|
||||
const health = await client.getHealth();
|
||||
expect(health.status).toBe("ok");
|
||||
});
|
||||
|
||||
it("creates client directly without spawn", () => {
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
expect(client).toBeInstanceOf(SandboxDaemonClient);
|
||||
});
|
||||
|
||||
it("handles authentication", async () => {
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl,
|
||||
token,
|
||||
});
|
||||
|
||||
const health = await client.getHealth();
|
||||
expect(health.status).toBe("ok");
|
||||
});
|
||||
|
||||
it("rejects invalid token on protected endpoints", async () => {
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl,
|
||||
token: "invalid-token",
|
||||
});
|
||||
|
||||
// Health endpoint may be open, but listing agents should require auth
|
||||
await expect(client.listAgents()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Runtime detection", () => {
|
||||
it("detects Node.js runtime", () => {
|
||||
expect(isNodeRuntime()).toBe(true);
|
||||
});
|
||||
});
|
||||
208
sdks/typescript/tests/sse-parser.test.ts
Normal file
208
sdks/typescript/tests/sse-parser.test.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import { describe, it, expect, vi, type Mock } from "vitest";
|
||||
import { SandboxDaemonClient } from "../src/client.ts";
|
||||
import type { UniversalEvent } from "../src/types.ts";
|
||||
|
||||
function createMockResponse(chunks: string[]): Response {
|
||||
let chunkIndex = 0;
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
if (chunkIndex < chunks.length) {
|
||||
controller.enqueue(encoder.encode(chunks[chunkIndex]));
|
||||
chunkIndex++;
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
function createMockFetch(chunks: string[]): Mock<typeof fetch> {
|
||||
return vi.fn<typeof fetch>().mockResolvedValue(createMockResponse(chunks));
|
||||
}
|
||||
|
||||
function createEvent(sequence: number): UniversalEvent {
|
||||
return {
|
||||
event_id: `evt-${sequence}`,
|
||||
sequence,
|
||||
session_id: "test-session",
|
||||
source: "agent",
|
||||
synthetic: false,
|
||||
time: new Date().toISOString(),
|
||||
type: "item.started",
|
||||
data: {
|
||||
item_id: `item-${sequence}`,
|
||||
kind: "message",
|
||||
role: "assistant",
|
||||
status: "in_progress",
|
||||
content: [],
|
||||
},
|
||||
} as UniversalEvent;
|
||||
}
|
||||
|
||||
describe("SSE Parser", () => {
|
||||
it("parses single SSE event", async () => {
|
||||
const event = createEvent(1);
|
||||
const mockFetch = createMockFetch([`data: ${JSON.stringify(event)}\n\n`]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].sequence).toBe(1);
|
||||
});
|
||||
|
||||
it("parses multiple SSE events", async () => {
|
||||
const event1 = createEvent(1);
|
||||
const event2 = createEvent(2);
|
||||
const mockFetch = createMockFetch([
|
||||
`data: ${JSON.stringify(event1)}\n\n`,
|
||||
`data: ${JSON.stringify(event2)}\n\n`,
|
||||
]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(2);
|
||||
expect(events[0].sequence).toBe(1);
|
||||
expect(events[1].sequence).toBe(2);
|
||||
});
|
||||
|
||||
it("handles chunked SSE data", async () => {
|
||||
const event = createEvent(1);
|
||||
const fullMessage = `data: ${JSON.stringify(event)}\n\n`;
|
||||
// Split in the middle of the message
|
||||
const mockFetch = createMockFetch([
|
||||
fullMessage.slice(0, 10),
|
||||
fullMessage.slice(10),
|
||||
]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].sequence).toBe(1);
|
||||
});
|
||||
|
||||
it("handles multiple events in single chunk", async () => {
|
||||
const event1 = createEvent(1);
|
||||
const event2 = createEvent(2);
|
||||
const mockFetch = createMockFetch([
|
||||
`data: ${JSON.stringify(event1)}\n\ndata: ${JSON.stringify(event2)}\n\n`,
|
||||
]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("ignores non-data lines", async () => {
|
||||
const event = createEvent(1);
|
||||
const mockFetch = createMockFetch([
|
||||
`: this is a comment\n`,
|
||||
`id: 1\n`,
|
||||
`data: ${JSON.stringify(event)}\n\n`,
|
||||
]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("handles CRLF line endings", async () => {
|
||||
const event = createEvent(1);
|
||||
const mockFetch = createMockFetch([
|
||||
`data: ${JSON.stringify(event)}\r\n\r\n`,
|
||||
]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("handles empty stream", async () => {
|
||||
const mockFetch = createMockFetch([]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
const events: UniversalEvent[] = [];
|
||||
for await (const e of client.streamEvents("test-session")) {
|
||||
events.push(e);
|
||||
}
|
||||
|
||||
expect(events).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("passes query parameters", async () => {
|
||||
const mockFetch = createMockFetch([]);
|
||||
|
||||
const client = new SandboxDaemonClient({
|
||||
baseUrl: "http://localhost:8080",
|
||||
fetch: mockFetch,
|
||||
});
|
||||
|
||||
// Consume the stream
|
||||
for await (const _ of client.streamEvents("test-session", { offset: 5 })) {
|
||||
// empty
|
||||
}
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining("offset=5"),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,15 +2,14 @@
|
|||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
|
|
|||
9
sdks/typescript/tsup.config.ts
Normal file
9
sdks/typescript/tsup.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
});
|
||||
8
sdks/typescript/vitest.config.ts
Normal file
8
sdks/typescript/vitest.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["tests/**/*.test.ts"],
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue