Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import {
OneCLIRequestError,
toOneCLIError,
} from "../errors.js";
import type { CreateAgentInput, CreateAgentResponse } from "./types.js";
import type {
CreateAgentInput,
CreateAgentResponse,
EnsureAgentResponse,
} from "./types.js";

export class AgentsClient {
private baseUrl: string;
Expand Down Expand Up @@ -57,4 +61,26 @@ export class AgentsClient {
throw toOneCLIError(error);
}
};

/**
* Ensure an agent exists. Creates it if missing, returns normally if it already exists.
* Unlike `createAgent`, this method treats a 409 conflict as success.
*/
ensureAgent = async (
input: CreateAgentInput,
): Promise<EnsureAgentResponse> => {
try {
await this.createAgent(input);
return { name: input.name, identifier: input.identifier, created: true };
} catch (error) {
if (error instanceof OneCLIRequestError && error.statusCode === 409) {
return {
name: input.name,
identifier: input.identifier,
created: false,
};
}
throw error;
}
};
}
7 changes: 7 additions & 0 deletions src/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ export interface CreateAgentResponse {
identifier: string;
createdAt: string;
}

export interface EnsureAgentResponse {
name: string;
identifier: string;
/** Whether the agent was newly created. `false` if it already existed. */
created: boolean;
}
8 changes: 8 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import type {
CreateAgentInput,
CreateAgentResponse,
EnsureAgentResponse,
} from "./agents/types.js";

const DEFAULT_URL = "https://app.onecli.sh";
Expand Down Expand Up @@ -50,4 +51,11 @@ export class OneCLI {
createAgent = (input: CreateAgentInput): Promise<CreateAgentResponse> => {
return this.agentsClient.createAgent(input);
};

/**
* Ensure an agent exists. Creates it if missing, returns normally if it already exists.
*/
ensureAgent = (input: CreateAgentInput): Promise<EnsureAgentResponse> => {
return this.agentsClient.ensureAgent(input);
};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export type {
export type {
CreateAgentInput,
CreateAgentResponse,
EnsureAgentResponse,
} from "./agents/types.js";
86 changes: 85 additions & 1 deletion test/agents/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ describe("AgentsClient", () => {
).rejects.toThrow("fetch failed");
});

it("re-throws OneCLIRequestError without wrapping", async () => {
it("re-throws OneCLIRequestError on 500 without wrapping", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response("", { status: 500, statusText: "Internal Server Error" }),
);
Expand All @@ -180,4 +180,88 @@ describe("AgentsClient", () => {
expect((err as OneCLIRequestError).name).toBe("OneCLIRequestError");
});
});

describe("ensureAgent", () => {
it("returns created: true when agent is newly created", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify(MOCK_AGENT), { status: 201 }),
);

const client = new AgentsClient(
"http://localhost:3000",
"oc_test",
5000,
);
const result = await client.ensureAgent({
name: "My Agent",
identifier: "my-agent",
});

expect(result).toEqual({
name: "My Agent",
identifier: "my-agent",
created: true,
});
});

it("returns created: false when agent already exists (409)", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify({ error: "identifier already exists" }),
{ status: 409, statusText: "Conflict" },
),
);

const client = new AgentsClient(
"http://localhost:3000",
"oc_test",
5000,
);
const result = await client.ensureAgent({
name: "My Agent",
identifier: "my-agent",
});

expect(result).toEqual({
name: "My Agent",
identifier: "my-agent",
created: false,
});
});

it("throws on non-409 errors", async () => {
fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
statusText: "Unauthorized",
}),
);

const client = new AgentsClient(
"http://localhost:3000",
"oc_bad",
5000,
);

await expect(
client.ensureAgent({ name: "Test", identifier: "test" }),
).rejects.toThrow(OneCLIRequestError);
});

it("throws on network errors", async () => {
fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockRejectedValue(new TypeError("fetch failed"));

const client = new AgentsClient(
"http://localhost:3000",
"oc_test",
5000,
);

await expect(
client.ensureAgent({ name: "Test", identifier: "test" }),
).rejects.toThrow(OneCLIError);
});
});
});
Loading