summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-http/src/app.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-05 21:20:12 +0900
committerAdam Malczewski <[email protected]>2026-06-05 21:20:12 +0900
commit7fb3269c698ae583ea7997ce206c4ae252fd3218 (patch)
tree247d03408ecccd633290ea56b1b08811ebe460ec /packages/transport-http/src/app.test.ts
parent4283d1f8a0bc3953e65962a2364c903d0015f047 (diff)
downloaddispatch-7fb3269c698ae583ea7997ce206c4ae252fd3218.tar.gz
dispatch-7fb3269c698ae583ea7997ce206c4ae252fd3218.zip
feat(backend): credential-store + model selection/catalog (GET /models) + per-turn cwd through orchestrator/transport/host-bin
Diffstat (limited to 'packages/transport-http/src/app.test.ts')
-rw-r--r--packages/transport-http/src/app.test.ts152
1 files changed, 145 insertions, 7 deletions
diff --git a/packages/transport-http/src/app.test.ts b/packages/transport-http/src/app.test.ts
index 9763605..38089e4 100644
--- a/packages/transport-http/src/app.test.ts
+++ b/packages/transport-http/src/app.test.ts
@@ -1,7 +1,7 @@
import type { AgentEvent } from "@dispatch/kernel";
import { describe, expect, it } from "vitest";
import { createApp } from "./app.js";
-import type { SessionOrchestrator } from "./seam.js";
+import type { CredentialStore, SessionOrchestrator } from "./seam.js";
function createFakeOrchestrator(events: AgentEvent[]): SessionOrchestrator {
return {
@@ -13,6 +13,22 @@ function createFakeOrchestrator(events: AgentEvent[]): SessionOrchestrator {
};
}
+function createCapturingOrchestrator(): SessionOrchestrator & {
+ received: Parameters<SessionOrchestrator["handleMessage"]>[0] | undefined;
+} {
+ const state: {
+ received: Parameters<SessionOrchestrator["handleMessage"]>[0] | undefined;
+ } = { received: undefined };
+ return {
+ get received() {
+ return state.received;
+ },
+ async handleMessage(input) {
+ state.received = input;
+ },
+ };
+}
+
function createThrowingOrchestrator(error: Error): SessionOrchestrator {
return {
async handleMessage() {
@@ -21,9 +37,34 @@ function createThrowingOrchestrator(error: Error): SessionOrchestrator {
};
}
+function createFakeCredentialStore(models: string[]): CredentialStore {
+ return {
+ resolve() {
+ return undefined;
+ },
+ async listCatalog() {
+ return models;
+ },
+ };
+}
+
+function createThrowingCredentialStore(error: Error): CredentialStore {
+ return {
+ resolve() {
+ return undefined;
+ },
+ async listCatalog() {
+ throw error;
+ },
+ };
+}
+
describe("GET /health", () => {
it("returns ok", async () => {
- const app = createApp({ orchestrator: createFakeOrchestrator([]) });
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore([]),
+ });
const res = await app.request("/health");
expect(res.status).toBe(200);
const body = await res.json();
@@ -31,9 +72,47 @@ describe("GET /health", () => {
});
});
+describe("GET /models", () => {
+ it("returns model catalog", async () => {
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore(["opencode/m1", "openai/gpt-4"]),
+ });
+ const res = await app.request("/models");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as { models: readonly string[] };
+ expect(body.models).toEqual(["opencode/m1", "openai/gpt-4"]);
+ });
+
+ it("returns empty array when no models", async () => {
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore([]),
+ });
+ const res = await app.request("/models");
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as { models: readonly string[] };
+ expect(body.models).toEqual([]);
+ });
+
+ it("returns 502 when listCatalog throws", async () => {
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createThrowingCredentialStore(new Error("db down")),
+ });
+ const res = await app.request("/models");
+ expect(res.status).toBe(502);
+ const body = (await res.json()) as { error: string };
+ expect(body.error).toContain("Failed to retrieve model catalog");
+ });
+});
+
describe("POST /chat", () => {
it("returns 400 for invalid JSON", async () => {
- const app = createApp({ orchestrator: createFakeOrchestrator([]) });
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore([]),
+ });
const res = await app.request("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -43,7 +122,10 @@ describe("POST /chat", () => {
});
it("returns 400 for missing message", async () => {
- const app = createApp({ orchestrator: createFakeOrchestrator([]) });
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore([]),
+ });
const res = await app.request("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -55,7 +137,10 @@ describe("POST /chat", () => {
});
it("returns 400 for empty message", async () => {
- const app = createApp({ orchestrator: createFakeOrchestrator([]) });
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore([]),
+ });
const res = await app.request("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -71,7 +156,10 @@ describe("POST /chat", () => {
{ type: "text-delta", conversationId: "tab1", turnId: "turn1", delta: " world" },
{ type: "done", conversationId: "tab1", turnId: "turn1", reason: "stop" },
];
- const app = createApp({ orchestrator: createFakeOrchestrator(events) });
+ const app = createApp({
+ orchestrator: createFakeOrchestrator(events),
+ credentialStore: createFakeCredentialStore([]),
+ });
const res = await app.request("/chat", {
method: "POST",
@@ -100,6 +188,7 @@ describe("POST /chat", () => {
orchestrator: createFakeOrchestrator([
{ type: "done", conversationId: "tab1", turnId: "turn1", reason: "stop" },
]),
+ credentialStore: createFakeCredentialStore([]),
generateId: () => "generated-uuid",
});
@@ -116,6 +205,7 @@ describe("POST /chat", () => {
it("emits error event when orchestrator throws", async () => {
const app = createApp({
orchestrator: createThrowingOrchestrator(new Error("provider unavailable")),
+ credentialStore: createFakeCredentialStore([]),
});
const res = await app.request("/chat", {
@@ -139,7 +229,10 @@ describe("POST /chat", () => {
});
it("handles empty event list", async () => {
- const app = createApp({ orchestrator: createFakeOrchestrator([]) });
+ const app = createApp({
+ orchestrator: createFakeOrchestrator([]),
+ credentialStore: createFakeCredentialStore([]),
+ });
const res = await app.request("/chat", {
method: "POST",
@@ -151,4 +244,49 @@ describe("POST /chat", () => {
const text = await res.text();
expect(text).toBe("");
});
+
+ it("forwards modelName and cwd to orchestrator", async () => {
+ const cap = createCapturingOrchestrator();
+ const app = createApp({
+ orchestrator: cap,
+ credentialStore: createFakeCredentialStore([]),
+ });
+
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ message: "hi",
+ conversationId: "conv1",
+ model: "opencode/m1",
+ cwd: "/tmp",
+ }),
+ });
+
+ expect(res.status).toBe(200);
+ expect(cap.received).toBeDefined();
+ expect(cap.received?.conversationId).toBe("conv1");
+ expect(cap.received?.text).toBe("hi");
+ expect(cap.received?.modelName).toBe("opencode/m1");
+ expect(cap.received?.cwd).toBe("/tmp");
+ });
+
+ it("omits modelName and cwd when not provided", async () => {
+ const cap = createCapturingOrchestrator();
+ const app = createApp({
+ orchestrator: cap,
+ credentialStore: createFakeCredentialStore([]),
+ });
+
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: "hi", conversationId: "conv1" }),
+ });
+
+ expect(res.status).toBe(200);
+ expect(cap.received).toBeDefined();
+ expect(cap.received?.modelName).toBeUndefined();
+ expect(cap.received?.cwd).toBeUndefined();
+ });
});