切换到使用pg数据库

This commit is contained in:
2026-05-28 18:22:39 +08:00
parent 4c47841483
commit 5ac50bfeaa
38 changed files with 2760 additions and 1331 deletions
+48 -8
View File
@@ -1,22 +1,21 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ConversationStore } from "../../src/conversations/store.js";
import { createTestDatabase } from "../helpers/postgres.js";
describe("ConversationStore", () => {
let tempDir: string;
let store: ConversationStore;
let dispose: (() => Promise<void>) | undefined;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "tjwater-conversation-"));
store = new ConversationStore(tempDir);
const testDb = await createTestDatabase();
store = new ConversationStore(testDb.db);
dispose = testDb.dispose;
await store.initialize();
});
afterEach(async () => {
await rm(tempDir, { force: true, recursive: true });
await dispose?.();
});
it("issues backend-managed session ids when absent", async () => {
@@ -58,7 +57,48 @@ describe("ConversationStore", () => {
},
"existing-session",
);
expect(fetched?.sessionScopeKey).toBe(record.sessionScopeKey);
expect(fetched?.sessionId).toBe(record.sessionId);
expect(fetched?.title).toBe("新标题");
});
it("tracks streaming state and clears it with runtime-session guarding", async () => {
const { record } = await store.ensure({
actorKey: "actor-3",
projectId: "project-3",
projectKey: "project-key-3",
sessionId: "streaming-session",
userId: "user-3",
});
const streaming = await store.markStreaming(record, "runtime-1");
expect(streaming.isStreaming).toBe(true);
expect(streaming.streamingStartedAt).toBeDefined();
const skipped = await store.clearStreaming(record.sessionId, "runtime-2");
expect(skipped).toBeNull();
const stillStreaming = await store.get(
{
actorKey: "actor-3",
projectId: "project-3",
projectKey: "project-key-3",
userId: "user-3",
},
record.sessionId,
);
expect(stillStreaming?.isStreaming).toBe(true);
await store.clearStreaming(record.sessionId, "runtime-1");
const cleared = await store.get(
{
actorKey: "actor-3",
projectId: "project-3",
projectKey: "project-key-3",
userId: "user-3",
},
record.sessionId,
);
expect(cleared?.isStreaming).toBe(false);
expect(cleared?.streamingStartedAt).toBeUndefined();
});
});
+17
View File
@@ -0,0 +1,17 @@
import { randomUUID } from "node:crypto";
import { AgentDatabase } from "../../src/db/index.js";
export const createTestDatabase = async () => {
const schema = `test_${randomUUID().replace(/-/g, "")}`;
const db = new AgentDatabase({ schema });
await db.initialize();
return {
db,
schema,
async dispose() {
await db.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
await db.close();
},
};
};
+45 -47
View File
@@ -1,67 +1,53 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ConversationStore } from "../../src/conversations/store.js";
import { SessionHistoryStore } from "../../src/history/store.js";
import { createTestDatabase } from "../helpers/postgres.js";
describe("SessionHistoryStore", () => {
let tempDir: string;
let conversationStore: ConversationStore;
let store: SessionHistoryStore;
let dispose: (() => Promise<void>) | undefined;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "tjwater-history-"));
store = new SessionHistoryStore(tempDir);
await store.initialize();
const testDb = await createTestDatabase();
conversationStore = new ConversationStore(testDb.db);
store = new SessionHistoryStore(testDb.db);
dispose = testDb.dispose;
await Promise.all([conversationStore.initialize(), store.initialize()]);
});
afterEach(async () => {
await rm(tempDir, { force: true, recursive: true });
await dispose?.();
});
it("falls back to legacy runtime-session transcripts by client session id and migrates on append", async () => {
await writeFile(
join(tempDir, "actor-1__project-1__runtime-session-1.json"),
JSON.stringify(
{
actorKey: "actor-1",
clientSessionId: "thread-1",
projectKey: "project-1",
sessionId: "runtime-session-1",
turns: [
{
id: "turn-1",
assistantMessage: "先检查泵站流量。",
timestamp: "2026-05-21T00:00:00.000Z",
toolCallCount: 1,
userMessage: "帮我看一下当前异常。",
},
],
updatedAt: "2026-05-21T00:00:00.000Z",
},
null,
2,
),
"utf8",
);
it("stores ordered turns for the durable conversation id", async () => {
await conversationStore.ensure({
actorKey: "actor-1",
projectId: "project-1",
projectKey: "project-1",
sessionId: "thread-1",
userId: "user-1",
});
const recentTurns = await store.getRecentTurns(
const first = await store.appendTurn(
{
actorKey: "actor-1",
clientSessionId: "thread-1",
projectKey: "project-1",
sessionId: "thread-1",
},
5,
{
assistantMessage: "先检查泵站流量。",
toolCallCount: 1,
userMessage: "帮我看一下当前异常。",
},
);
expect(recentTurns).toHaveLength(1);
expect(recentTurns[0]?.userMessage).toBe("帮我看一下当前异常。");
expect(first.turns).toHaveLength(1);
const transcript = await store.appendTurn(
const second = await store.appendTurn(
{
actorKey: "actor-1",
clientSessionId: "thread-1",
projectKey: "project-1",
sessionId: "thread-1",
},
@@ -72,15 +58,31 @@ describe("SessionHistoryStore", () => {
},
);
expect(transcript.sessionId).toBe("thread-1");
expect(transcript.turns).toHaveLength(2);
expect(second.sessionId).toBe("thread-1");
expect(second.turns).toHaveLength(2);
expect(second.turns[0]?.userMessage).toBe("帮我看一下当前异常。");
expect(second.turns[1]?.assistantMessage).toBe("已经定位到 3 条疑似异常支路。");
});
it("clones only the kept prefix when forking a thread", async () => {
await conversationStore.ensure({
actorKey: "actor-2",
projectId: "project-2",
projectKey: "project-2",
sessionId: "thread-source",
userId: "user-2",
});
await conversationStore.ensure({
actorKey: "actor-2",
projectId: "project-2",
projectKey: "project-2",
sessionId: "thread-fork",
userId: "user-2",
});
await store.appendTurn(
{
actorKey: "actor-2",
clientSessionId: "thread-source",
projectKey: "project-2",
sessionId: "thread-source",
},
@@ -93,7 +95,6 @@ describe("SessionHistoryStore", () => {
await store.appendTurn(
{
actorKey: "actor-2",
clientSessionId: "thread-source",
projectKey: "project-2",
sessionId: "thread-source",
},
@@ -107,13 +108,11 @@ describe("SessionHistoryStore", () => {
const cloned = await store.cloneThread(
{
actorKey: "actor-2",
clientSessionId: "thread-source",
projectKey: "project-2",
sessionId: "thread-source",
},
{
actorKey: "actor-2",
clientSessionId: "thread-fork",
projectKey: "project-2",
sessionId: "thread-fork",
},
@@ -126,7 +125,6 @@ describe("SessionHistoryStore", () => {
const forkRecentTurns = await store.getRecentTurns(
{
actorKey: "actor-2",
clientSessionId: "thread-fork",
projectKey: "project-2",
sessionId: "thread-fork",
},
+41 -310
View File
@@ -3,46 +3,65 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ConversationStore } from "../../src/conversations/store.js";
import { ResultReferenceResolver } from "../../src/results/resolver.js";
import {
RESULT_REFERENCE_KIND,
RESULT_REFERENCE_SOURCE,
ResultReferenceStore,
} from "../../src/results/store.js";
import { createTestDatabase } from "../helpers/postgres.js";
describe("ResultReferenceResolver", () => {
let tempDir: string;
let payloadDir: string;
let conversationStore: ConversationStore;
let store: ResultReferenceStore;
let resolver: ResultReferenceResolver;
let dispose: (() => Promise<void>) | undefined;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "tjwater-result-ref-"));
store = new ResultReferenceStore(tempDir, 60_000);
payloadDir = await mkdtemp(join(tmpdir(), "tjwater-result-ref-"));
const testDb = await createTestDatabase();
conversationStore = new ConversationStore(testDb.db);
store = new ResultReferenceStore(testDb.db, payloadDir, 60_000);
resolver = new ResultReferenceResolver(store);
await store.initialize();
dispose = async () => {
await testDb.dispose();
await rm(payloadDir, { force: true, recursive: true });
};
await Promise.all([conversationStore.initialize(), store.initialize()]);
});
afterEach(async () => {
await rm(tempDir, { force: true, recursive: true });
await dispose?.();
});
it("stores metadata for new referenced results and resolves them", async () => {
it("stores referenced results as metadata plus an external payload file", async () => {
await conversationStore.ensure({
actorKey: "actor-1",
projectId: "project-1",
projectKey: "project-key-1",
sessionId: "client-1",
userId: "user-1",
});
const record = await resolver.register({
actorKey: "actor-1",
clientSessionId: "client-1",
data: [{ id: "J1" }, { id: "J2" }],
kind: RESULT_REFERENCE_KIND.dynamicHttpResult,
projectId: "project-1",
projectKey: "project-key-1",
schemaVersion: 1,
sessionId: "session-1",
sessionId: "client-1",
source: RESULT_REFERENCE_SOURCE.dynamicHttp,
traceId: "trace-1",
});
expect(record.kind).toBe(RESULT_REFERENCE_KIND.dynamicHttpResult);
expect(record.schemaVersion).toBe(1);
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.dynamicHttp);
expect(record.payloadPath).toContain("/payloads/");
const payload = JSON.parse(await readFile(record.payloadPath!, "utf8")) as {
data?: unknown;
};
expect(payload.data).toEqual([{ id: "J1" }, { id: "J2" }]);
const result = await resolver.getAuthorized(
record.resultRef,
@@ -55,124 +74,21 @@ describe("ResultReferenceResolver", () => {
},
);
expect(result).not.toBeNull();
expect(result?.kind).toBe(RESULT_REFERENCE_KIND.dynamicHttpResult);
expect(result?.schema_version).toBe(1);
expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.dynamicHttp);
expect(result?.data).toEqual([{ id: "J1" }]);
});
it("keeps legacy result refs readable while defaulting metadata", async () => {
const legacyRef = "res-aaaaaaaaaaaaaaaa";
await writeFile(
join(tempDir, `${legacyRef}.json`),
JSON.stringify(
{
resultRef: legacyRef,
actorKey: "actor-legacy",
clientSessionId: "client-legacy",
createdAt: "2026-05-21T00:00:00.000Z",
data: { nodes: ["J1"] },
preview: {
count: 1,
fields: ["nodes"],
sample: { nodes: ["J1"] },
summary: "object<1 fields>",
},
projectId: "project-legacy",
projectKey: "project-key-legacy",
sessionId: "session-legacy",
sizeBytes: 16,
traceId: "trace-legacy",
},
null,
2,
),
"utf8",
);
const record = await store.getAuthorizedRecord(legacyRef, {
actorKey: "actor-legacy",
projectId: "project-legacy",
it("reuses an existing external render payload file as the payload locator", async () => {
await conversationStore.ensure({
actorKey: "actor-3",
projectId: "project-3",
projectKey: "project-key-3",
sessionId: "client-3",
userId: "user-3",
});
expect(record).not.toBeNull();
expect(record?.kind).toBe(RESULT_REFERENCE_KIND.dynamicHttpResult);
expect(record?.schemaVersion).toBe(1);
expect(record?.source).toBe(RESULT_REFERENCE_SOURCE.legacy);
});
it("rejects malformed refs, mismatched kinds, and auth mismatches", async () => {
const malformedRef = "res-bbbbbbbbbbbbbbbb";
await writeFile(
join(tempDir, `${malformedRef}.json`),
JSON.stringify(
{
resultRef: malformedRef,
createdAt: "2026-05-21T00:00:00.000Z",
data: { value: 1 },
preview: {
count: 1,
fields: ["value"],
sample: { value: 1 },
summary: "object<1 fields>",
},
projectId: "project-1",
projectKey: "project-key-1",
sessionId: "session-1",
sizeBytes: 10,
traceId: "trace-1",
},
null,
2,
),
"utf8",
);
const malformed = await store.getAuthorizedRecord(malformedRef, {
actorKey: "actor-1",
projectId: "project-1",
});
expect(malformed).toBeNull();
const renderRecord = await resolver.register({
actorKey: "actor-2",
clientSessionId: "client-2",
data: {
node_area_map: {
J1: "DMA-1",
},
},
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
projectId: "project-2",
projectKey: "project-key-2",
schemaVersion: 1,
sessionId: "session-2",
source: RESULT_REFERENCE_SOURCE.agentGenerated,
traceId: "trace-2",
});
const wrongKind = await resolver.getFullAuthorized(
renderRecord.resultRef,
{
actorKey: "actor-2",
projectId: "project-2",
},
{
expectedKind: RESULT_REFERENCE_KIND.dynamicHttpResult,
},
);
expect(wrongKind).toBeNull();
const wrongActor = await resolver.getFullAuthorized(renderRecord.resultRef, {
actorKey: "actor-other",
projectId: "project-2",
});
expect(wrongActor).toBeNull();
});
it("registers render refs from local wrapper files and normalizes payloads", async () => {
const filePath = join(tempDir, "render-wrapper.json");
const filePath = join(payloadDir, "render-wrapper.json");
await writeFile(
filePath,
JSON.stringify(
@@ -190,12 +106,7 @@ describe("ResultReferenceResolver", () => {
J2: 2,
},
area_ids: ["DMA-1", " DMA-2 "],
area_colors: {
"DMA-1": "#ff0000",
"DMA-2": "#00ff00",
},
},
createdAt: "2026-05-21T00:00:00.000Z",
},
null,
2,
@@ -205,22 +116,21 @@ describe("ResultReferenceResolver", () => {
const record = await resolver.registerRenderPayloadFile(filePath, {
actorKey: "actor-3",
clientSessionId: "client-3",
projectId: "project-3",
projectKey: "project-key-3",
sessionId: "session-3",
sessionId: "client-3",
source: RESULT_REFERENCE_SOURCE.migration,
traceId: "trace-3",
});
expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.migration);
expect(record.payloadPath).toBe(filePath);
const result = await resolver.getFullAuthorized(
record.resultRef,
{
actorKey: "actor-3",
projectId: "project-3",
sessionId: "client-3",
},
{
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
@@ -233,185 +143,6 @@ describe("ResultReferenceResolver", () => {
J2: "2",
},
area_ids: ["DMA-1", "DMA-2"],
area_colors: {
"DMA-1": "#ff0000",
"DMA-2": "#00ff00",
},
});
});
it("repairs wrapper files that omit metadata and location", async () => {
const filePath = join(tempDir, "render-wrapper-missing-fields.json");
await writeFile(
filePath,
JSON.stringify(
{
data: {
node_area_map: {
J1: "DMA-1",
},
},
createdAt: "2026-05-21T00:00:00.000Z",
},
null,
2,
),
"utf8",
);
const record = await resolver.registerRenderPayloadFile(filePath, {
actorKey: "actor-4",
clientSessionId: "client-4",
projectId: "project-4",
projectKey: "project-key-4",
sessionId: "session-4",
source: RESULT_REFERENCE_SOURCE.migration,
traceId: "trace-4",
});
expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
const repaired = JSON.parse(await readFile(filePath, "utf8")) as {
metadata?: Record<string, unknown>;
location?: Record<string, unknown>;
};
expect(repaired.metadata).toEqual({
createdAt: "2026-05-21T00:00:00.000Z",
projectId: "project-4",
});
expect(repaired.location).toEqual({
file_path: filePath,
});
});
it("repairs wrapper files whose location points elsewhere", async () => {
const filePath = join(tempDir, "render-wrapper-wrong-location.json");
await writeFile(
filePath,
JSON.stringify(
{
metadata: {
createdAt: "2026-05-21T00:00:00.000Z",
},
location: {
file_path: "/tmp/elsewhere.json",
source: "legacy",
},
data: {
node_area_map: {
J1: "DMA-1",
},
},
},
null,
2,
),
"utf8",
);
await resolver.registerRenderPayloadFile(filePath, {
actorKey: "actor-4",
clientSessionId: "client-4",
projectId: "project-4",
projectKey: "project-key-4",
sessionId: "session-4",
source: RESULT_REFERENCE_SOURCE.migration,
traceId: "trace-4",
});
const repaired = JSON.parse(await readFile(filePath, "utf8")) as {
metadata?: Record<string, unknown>;
location?: Record<string, unknown>;
};
expect(repaired.metadata).toEqual({
createdAt: "2026-05-21T00:00:00.000Z",
projectId: "project-4",
});
expect(repaired.location).toEqual({
file_path: filePath,
source: "legacy",
});
});
it("resolves legacy render payload files when callers include the json suffix", async () => {
const legacyRef = "res-c2fcee33-577e";
await writeFile(
join(tempDir, `${legacyRef}.json`),
JSON.stringify(
{
data: {
node_area_map: {
J1: "DMA-1",
J2: 2,
},
area_ids: ["DMA-1"],
},
createdAt: "2026-05-21T00:00:00.000Z",
projectId: "project-legacy-render",
},
null,
2,
),
"utf8",
);
const result = await resolver.getFullAuthorized(
`${legacyRef}.json`,
{
actorKey: "actor-legacy-render",
clientSessionId: "chat-legacy-render",
projectId: "project-legacy-render",
},
{
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
},
);
expect(result?.result_ref).toBe(legacyRef);
expect(result?.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.legacy);
expect(result?.data).toEqual({
node_area_map: {
J1: "DMA-1",
J2: "2",
},
area_ids: ["DMA-1"],
});
});
it("keeps legacy render payload files scoped to their project", async () => {
const legacyRef = "res-dddddddddddddddd";
await writeFile(
join(tempDir, `${legacyRef}.json`),
JSON.stringify(
{
data: {
node_area_map: {
J1: "DMA-1",
},
},
projectId: "project-allowed",
},
null,
2,
),
"utf8",
);
const result = await resolver.getFullAuthorized(
legacyRef,
{
actorKey: "actor-legacy-render",
clientSessionId: "chat-legacy-render",
projectId: "project-denied",
},
{
expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
},
);
expect(result).toBeNull();
});
});
+52
View File
@@ -0,0 +1,52 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { ConversationStore } from "../../src/conversations/store.js";
import { RuntimeSessionStore } from "../../src/session/runtimeSessionStore.js";
import { createTestDatabase } from "../helpers/postgres.js";
describe("RuntimeSessionStore", () => {
let conversationStore: ConversationStore;
let store: RuntimeSessionStore;
let dispose: (() => Promise<void>) | undefined;
beforeEach(async () => {
const testDb = await createTestDatabase();
conversationStore = new ConversationStore(testDb.db);
store = new RuntimeSessionStore(testDb.db);
dispose = testDb.dispose;
await Promise.all([conversationStore.initialize(), store.initialize()]);
});
afterEach(async () => {
await dispose?.();
});
it("stores and releases runtime sessions by runtime session id", async () => {
await conversationStore.ensure({
actorKey: "actor-1",
projectId: "project-1",
projectKey: "project-1",
sessionId: "chat-session-1",
userId: "user-1",
});
await store.write({
runtimeSessionId: "runtime-session-1",
actorKey: "actor-1",
allowLearningWrite: true,
sessionId: "chat-session-1",
learningMode: "interactive",
projectId: "project-id-1",
projectKey: "project-1",
traceId: "trace-1",
});
const runtimeContext = await store.read("runtime-session-1");
expect(runtimeContext?.sessionId).toBe("chat-session-1");
expect(runtimeContext?.runtimeSessionId).toBe("runtime-session-1");
await store.release("runtime-session-1");
expect(await store.read("runtime-session-1")).toBeNull();
expect((await store.read("runtime-session-1", { includeReleased: true }))?.releasedAt).toBeDefined();
});
});
-51
View File
@@ -1,51 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
buildToolSessionScopeKey,
ToolSessionContextStore,
} from "../../src/session/toolContextStore.js";
describe("ToolSessionContextStore", () => {
let tempDir: string;
let store: ToolSessionContextStore;
beforeEach(async () => {
tempDir = await mkdtemp(join(tmpdir(), "tjwater-tool-context-"));
store = new ToolSessionContextStore(tempDir);
await store.initialize();
});
afterEach(async () => {
await rm(tempDir, { force: true, recursive: true });
});
it("writes interactive aliases under scoped session keys", async () => {
const sessionScopeKey = buildToolSessionScopeKey(
"actor-1",
"project-1",
"chat-session-1",
);
await store.write({
actorKey: "actor-1",
allowLearningWrite: true,
clientSessionId: "chat-session-1",
learningMode: "interactive",
projectId: "project-id-1",
projectKey: "project-1",
sessionId: "runtime-session-1",
sessionScopeKey,
traceId: "trace-1",
});
const runtimeContext = await store.read("runtime-session-1");
const scopedContext = await store.read(sessionScopeKey);
expect(runtimeContext?.clientSessionId).toBe("chat-session-1");
expect(scopedContext?.sessionScopeKey).toBe(sessionScopeKey);
expect(scopedContext?.sessionId).toBe("runtime-session-1");
});
});
+110
View File
@@ -0,0 +1,110 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ResultReferenceStore } from "../../src/results/store.js";
import { DynamicHttpExecutor } from "../../src/tools/dynamicHttpExecutor.js";
import { createTestDatabase } from "../helpers/postgres.js";
describe("DynamicHttpExecutor", () => {
const originalFetch = globalThis.fetch;
let payloadDir: string;
let store: ResultReferenceStore;
let executor: DynamicHttpExecutor;
let dispose: (() => Promise<void>) | undefined;
beforeEach(async () => {
payloadDir = await mkdtemp(join(tmpdir(), "tjwater-dynamic-http-"));
const testDb = await createTestDatabase();
store = new ResultReferenceStore(testDb.db, payloadDir, 60_000);
executor = new DynamicHttpExecutor(store);
dispose = async () => {
globalThis.fetch = originalFetch;
await testDb.dispose();
await rm(payloadDir, { force: true, recursive: true });
};
await store.initialize();
});
afterEach(async () => {
await dispose?.();
});
it("sends JSON body for POST requests while preserving query arguments", async () => {
let capturedUrl = "";
let capturedInit: RequestInit | undefined;
globalThis.fetch = Object.assign(
async (input: URL | RequestInfo, init?: RequestInit | BunFetchRequestInit) => {
capturedUrl = String(input);
capturedInit = init;
return new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
originalFetch,
);
const result = await executor.execute(
{
path: "/api/v1/runproject/simulate",
method: "POST",
arguments: { network: "demo-network" },
body: { duration_minutes: 15 },
},
{
accessToken: "token-1",
actorKey: "actor-1",
projectId: "project-1",
projectKey: "project-key-1",
sessionId: "session-1",
traceId: "trace-1",
},
);
const url = new URL(capturedUrl);
const headers = new Headers(capturedInit?.headers);
expect(url.pathname).toBe("/api/v1/runproject/simulate");
expect(url.searchParams.get("network")).toBe("demo-network");
expect(capturedInit?.method).toBe("POST");
expect(headers.get("content-type")).toBe("application/json");
expect(headers.get("authorization")).toBe("Bearer token-1");
expect(headers.get("x-project-id")).toBe("project-1");
expect(headers.get("x-trace-id")).toBe("trace-1");
expect(capturedInit?.body).toBe(JSON.stringify({ duration_minutes: 15 }));
expect(result).toEqual({
ok: true,
trace_id: "trace-1",
upstream: {
method: "POST",
path: "/api/v1/runproject/simulate",
status_code: 200,
},
result_mode: "inline",
result_size_bytes: expect.any(Number),
data: { status: "ok" },
});
});
it("rejects GET requests that include a body", async () => {
await expect(
executor.execute(
{
path: "/api/v1/runproject/",
method: "GET",
arguments: { network: "demo-network" },
body: { duration_minutes: 15 },
},
{
actorKey: "actor-1",
projectId: "project-1",
projectKey: "project-key-1",
sessionId: "session-1",
traceId: "trace-1",
},
),
).rejects.toThrow("GET requests do not support body");
});
});