Skip to content

Commit 7cbdbe0

Browse files
committed
Tests and verification
1 parent a3809bc commit 7cbdbe0

5 files changed

Lines changed: 172 additions & 1 deletion

File tree

bun.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/agents/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
"scripts": {
1515
"build": "tsc",
1616
"dev": "tsc --watch",
17-
"typecheck": "tsc --noEmit"
17+
"typecheck": "tsc --noEmit",
18+
"test": "bun test"
1819
},
1920
"dependencies": {
2021
"@stackforge/shared": "workspace:*"
2122
},
2223
"devDependencies": {
24+
"@types/bun": "^1.3.11",
2325
"@types/node": "^22.13.10",
2426
"typescript": "^5.7.3"
2527
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/// <reference types="bun" />
2+
import { afterEach, describe, expect, it } from "bun:test";
3+
import { OpenRouterProvider } from "../src/provider/openrouter.provider.js";
4+
5+
const originalFetch = globalThis.fetch;
6+
7+
afterEach(() => {
8+
globalThis.fetch = originalFetch;
9+
});
10+
11+
describe("OpenRouterProvider", () => {
12+
it("parses JSON response and exposes usage metrics", async () => {
13+
globalThis.fetch = async () =>
14+
new Response(
15+
JSON.stringify({
16+
choices: [{ message: { content: '{"projectName":"acme"}' } }],
17+
usage: { prompt_tokens: 120, completion_tokens: 80, total_tokens: 200 },
18+
model: "openai/gpt-4o-mini",
19+
}),
20+
{ status: 200, headers: { "content-type": "application/json" } },
21+
);
22+
23+
const provider = new OpenRouterProvider({ apiKey: "test-key" });
24+
25+
const result = await provider.call({
26+
agentName: "planner",
27+
input: { prompt: "build app", projectName: "acme" },
28+
options: {
29+
systemPrompt: "Return JSON",
30+
userPrompt: "{\"prompt\":\"build app\"}",
31+
model: "openai/gpt-4o-mini",
32+
maxInputTokens: 300,
33+
maxOutputTokens: 300,
34+
temperature: 0.1,
35+
},
36+
});
37+
38+
expect(result.output).toEqual({ projectName: "acme" });
39+
expect(result.inputTokens).toBe(120);
40+
expect(result.outputTokens).toBe(80);
41+
expect(result.tokensUsed).toBe(200);
42+
expect(result.model).toBe("openai/gpt-4o-mini");
43+
});
44+
45+
it("throws on non-JSON model content", async () => {
46+
globalThis.fetch = async () =>
47+
new Response(
48+
JSON.stringify({
49+
choices: [{ message: { content: "not-json" } }],
50+
}),
51+
{ status: 200, headers: { "content-type": "application/json" } },
52+
);
53+
54+
const provider = new OpenRouterProvider({ apiKey: "test-key" });
55+
56+
await expect(
57+
provider.call({
58+
agentName: "planner",
59+
input: { prompt: "build app", projectName: "acme" },
60+
options: {
61+
systemPrompt: "Return JSON",
62+
userPrompt: "{}",
63+
model: "openai/gpt-4o-mini",
64+
maxInputTokens: 300,
65+
maxOutputTokens: 300,
66+
temperature: 0.1,
67+
},
68+
}),
69+
).rejects.toThrow("OpenRouter returned non-JSON content");
70+
});
71+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/// <reference types="bun" />
2+
import { describe, expect, it } from "bun:test";
3+
import type { ProviderCallInput, ProviderCallOutput } from "../src/provider/provider.interface.js";
4+
import type { LLMProvider } from "../src/provider/provider.interface.js";
5+
import { runOrchestrator } from "../src/orchestrator/orchestrator.service.js";
6+
import { AgentCache } from "../src/cache/agent.cache.js";
7+
import type { SSEEvent } from "@stackforge/shared";
8+
9+
class InvalidPlannerProvider implements LLMProvider {
10+
readonly name = "invalid-planner";
11+
12+
async call({ agentName }: ProviderCallInput): Promise<ProviderCallOutput> {
13+
if (agentName === "planner") {
14+
return {
15+
output: { bad: true },
16+
tokensUsed: 42,
17+
durationMs: 5,
18+
inputTokens: 20,
19+
outputTokens: 22,
20+
model: "test-model",
21+
};
22+
}
23+
24+
return {
25+
output: {},
26+
tokensUsed: 1,
27+
durationMs: 1,
28+
inputTokens: 1,
29+
outputTokens: 0,
30+
model: "test-model",
31+
};
32+
}
33+
}
34+
35+
describe("Orchestrator failure propagation", () => {
36+
it("emits agent_failed and rejects when agent output schema validation fails", async () => {
37+
const events: SSEEvent[] = [];
38+
const provider = new InvalidPlannerProvider();
39+
40+
await expect(
41+
runOrchestrator({
42+
jobId: crypto.randomUUID(),
43+
prompt: "Build a task management platform",
44+
projectName: "taskflow",
45+
emit: (event) => events.push(event),
46+
provider,
47+
cache: new AgentCache(),
48+
}),
49+
).rejects.toBeDefined();
50+
51+
const started = events.find((event) => event.type === "agent_started" && event.agent === "planner");
52+
const failed = events.find((event) => event.type === "agent_failed" && event.agent === "planner");
53+
54+
expect(started).toBeDefined();
55+
expect(failed).toBeDefined();
56+
if (failed?.type === "agent_failed") {
57+
expect(failed.payload.error.length).toBeGreaterThan(0);
58+
}
59+
});
60+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/// <reference types="bun" />
2+
import { describe, it, expect } from "bun:test";
3+
import { optimizeAgentPayload } from "../src/optimizer/token.optimizer.js";
4+
import { AGENT_CONFIGS } from "../src/config/agent.configs.js";
5+
6+
describe("Token optimizer", () => {
7+
it("compresses oversized prompt input within configured input token limit", () => {
8+
const veryLongPrompt = "Build enterprise app. ".repeat(3000);
9+
10+
const result = optimizeAgentPayload("planner", {
11+
prompt: veryLongPrompt,
12+
projectName: "planner-test",
13+
});
14+
15+
expect(result.maxInputTokens).toBe(900);
16+
expect(result.estimatedInputTokens).toBeLessThanOrEqual(result.maxInputTokens);
17+
expect(result.compressionPasses).toBeGreaterThanOrEqual(1);
18+
expect(result.userPrompt.length).toBeLessThanOrEqual(result.maxInputTokens * 4);
19+
});
20+
21+
it("fails fast when remaining budget cannot satisfy minimum output tokens", () => {
22+
const original = AGENT_CONFIGS.devops.minOutputTokens;
23+
AGENT_CONFIGS.devops.minOutputTokens = AGENT_CONFIGS.devops.tokenBudget;
24+
25+
try {
26+
expect(() =>
27+
optimizeAgentPayload("devops", {
28+
prompt: "x".repeat(40000),
29+
stack: { backend: "express", database: "postgres" },
30+
entities: Array.from({ length: 200 }, (_, index) => ({ name: `Entity${index}` })),
31+
}),
32+
).toThrow("Insufficient output token budget");
33+
} finally {
34+
AGENT_CONFIGS.devops.minOutputTokens = original;
35+
}
36+
});
37+
});

0 commit comments

Comments
 (0)