切换到使用pg数据库
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user