refactor: remove legacy data compatibility
This commit is contained in:
+19
-80
@@ -1,10 +1,11 @@
|
||||
import { atomicWriteJson, readJsonFile } from "../utils/fileStore.js";
|
||||
import { readJsonFile } from "../utils/fileStore.js";
|
||||
import {
|
||||
type ResultReferenceKind,
|
||||
type ResultReferenceRecord,
|
||||
type ResultReferenceSource,
|
||||
type RetrievalContext,
|
||||
RESULT_REFERENCE_KIND,
|
||||
RESULT_REFERENCE_SOURCE,
|
||||
type ResultReferenceStore,
|
||||
} from "./store.js";
|
||||
|
||||
@@ -34,6 +35,7 @@ export type RenderJunctionPayload = {
|
||||
export class ResultReferenceResolver {
|
||||
constructor(private readonly store: ResultReferenceStore) {}
|
||||
|
||||
// Resolver 负责按结果类型做结构校验,Store 只关心授权和落盘。
|
||||
async register(input: RegisterResultReferenceInput) {
|
||||
const normalizedData = normalizeDataForKind(
|
||||
input.kind,
|
||||
@@ -66,15 +68,12 @@ export class ResultReferenceResolver {
|
||||
throw new Error(`render payload file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const payloadCandidate = normalizeRenderPayloadFile(raw, {
|
||||
filePath,
|
||||
projectId: input.projectId,
|
||||
});
|
||||
if (payloadCandidate.repaired) {
|
||||
await atomicWriteJson(filePath, payloadCandidate.file);
|
||||
const wrapper = normalizeRenderPayloadFile(raw, filePath);
|
||||
if (!wrapper) {
|
||||
throw new Error("render payload file must use the wrapped { metadata, location, data } format");
|
||||
}
|
||||
|
||||
const payload = extractRenderJunctionPayload(payloadCandidate.data);
|
||||
const payload = extractRenderJunctionPayload(wrapper.data);
|
||||
if (!payload) {
|
||||
throw new Error("render payload file does not contain a valid junction render payload");
|
||||
}
|
||||
@@ -84,6 +83,7 @@ export class ResultReferenceResolver {
|
||||
data: payload,
|
||||
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||
schemaVersion: 1,
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ export const extractRenderJunctionPayload = (
|
||||
return null;
|
||||
}
|
||||
|
||||
// 节点渲染结果只保留前端真正需要的映射字段,剔除空值并统一转为字符串。
|
||||
const nodeAreaMap = normalizeStringRecord(candidate.node_area_map);
|
||||
if (Object.keys(nodeAreaMap).length === 0) {
|
||||
return null;
|
||||
@@ -182,35 +183,18 @@ const normalizeDataForKind = (
|
||||
|
||||
const normalizeRenderPayloadFile = (
|
||||
value: unknown,
|
||||
context: { filePath: string; projectId?: string },
|
||||
): { data: unknown; file: Record<string, unknown>; repaired: boolean } => {
|
||||
filePath: string,
|
||||
): { data: unknown } | null => {
|
||||
if (!isRecord(value) || !("data" in value)) {
|
||||
return {
|
||||
data: value,
|
||||
file: {
|
||||
metadata: buildWrapperMetadata({}, value, context.projectId),
|
||||
location: buildWrapperLocation(undefined, context.filePath),
|
||||
data: value,
|
||||
},
|
||||
repaired: false,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = buildWrapperMetadata(value.metadata, value, context.projectId);
|
||||
const location = buildWrapperLocation(value.location, context.filePath);
|
||||
const next: Record<string, unknown> = {
|
||||
...value,
|
||||
metadata,
|
||||
location,
|
||||
};
|
||||
|
||||
return {
|
||||
data: next.data,
|
||||
file: next,
|
||||
repaired:
|
||||
JSON.stringify(metadata) !== JSON.stringify(value.metadata ?? null) ||
|
||||
JSON.stringify(location) !== JSON.stringify(value.location ?? null),
|
||||
};
|
||||
if (!isRecord(value.metadata) || !isRecord(value.location)) {
|
||||
return null;
|
||||
}
|
||||
if (value.location.file_path !== filePath) {
|
||||
return null;
|
||||
}
|
||||
return { data: value.data };
|
||||
};
|
||||
|
||||
const unwrapReferencePayload = (value: unknown): Record<string, unknown> | null => {
|
||||
@@ -232,48 +216,3 @@ const normalizeStringRecord = (value: Record<string, unknown>) =>
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const buildWrapperMetadata = (
|
||||
value: unknown,
|
||||
root: unknown,
|
||||
fallbackProjectId?: string,
|
||||
) => {
|
||||
const metadata = isRecord(value) ? { ...value } : {};
|
||||
const source = isRecord(root) ? root : {};
|
||||
|
||||
if (typeof metadata.createdAt !== "string" || metadata.createdAt.trim().length === 0) {
|
||||
const createdAt =
|
||||
typeof source.createdAt === "string" && source.createdAt.trim().length > 0
|
||||
? source.createdAt.trim()
|
||||
: new Date().toISOString();
|
||||
metadata.createdAt = createdAt;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof metadata.projectId !== "string" ||
|
||||
metadata.projectId.trim().length === 0
|
||||
) {
|
||||
const projectId =
|
||||
typeof source.projectId === "string" && source.projectId.trim().length > 0
|
||||
? source.projectId.trim()
|
||||
: fallbackProjectId;
|
||||
if (projectId) {
|
||||
metadata.projectId = projectId;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
};
|
||||
|
||||
const buildWrapperLocation = (value: unknown, filePath: string) => {
|
||||
if (isRecord(value)) {
|
||||
return {
|
||||
...value,
|
||||
file_path: filePath,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
file_path: filePath,
|
||||
};
|
||||
};
|
||||
|
||||
+6
-61
@@ -10,7 +10,6 @@ import {
|
||||
listJsonFiles,
|
||||
readJsonFile,
|
||||
removeFileIfExists,
|
||||
toProjectKey,
|
||||
} from "../utils/fileStore.js";
|
||||
|
||||
export const RESULT_REF_PATTERN = /^res-[a-f0-9-]{8,64}$/;
|
||||
@@ -22,8 +21,6 @@ export const RESULT_REFERENCE_KIND = {
|
||||
|
||||
export const RESULT_REFERENCE_SOURCE = {
|
||||
agentGenerated: "agent_generated",
|
||||
legacy: "legacy",
|
||||
migration: "migration",
|
||||
} as const;
|
||||
|
||||
export type ResultReferenceKind =
|
||||
@@ -133,6 +130,7 @@ export class ResultReferenceStore {
|
||||
source: input.source,
|
||||
traceId: input.traceId,
|
||||
};
|
||||
// result_ref 对外暴露短引用,完整数据落盘;这样可以避免大结果直接塞进模型上下文。
|
||||
await atomicWriteJson(this.filePath(resultRef), record);
|
||||
return record;
|
||||
}
|
||||
@@ -143,13 +141,13 @@ export class ResultReferenceStore {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawRecord = await readJsonFile<unknown>(this.filePath(normalizedResultRef));
|
||||
const record =
|
||||
normalizeResultReferenceRecord(rawRecord) ??
|
||||
normalizeLegacyRenderReferenceRecord(rawRecord, normalizedResultRef, context);
|
||||
const record = normalizeResultReferenceRecord(
|
||||
await readJsonFile<unknown>(this.filePath(normalizedResultRef)),
|
||||
);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
// 读取 result_ref 时按用户、项目和可选会话三层校验,防止跨项目/跨用户取数。
|
||||
if (record.actorKey !== context.actorKey) {
|
||||
return null;
|
||||
}
|
||||
@@ -202,6 +200,7 @@ export class ResultReferenceStore {
|
||||
if (!stats) {
|
||||
continue;
|
||||
}
|
||||
// TTL 以文件修改时间为准,清理长期无人访问的 result_ref 文件。
|
||||
if (now - stats.mtimeMs > this.ttlMs) {
|
||||
await removeFileIfExists(filePath);
|
||||
}
|
||||
@@ -303,60 +302,6 @@ const normalizeResultRef = (value: string) => {
|
||||
return match?.[1] ?? null;
|
||||
};
|
||||
|
||||
const normalizeLegacyRenderReferenceRecord = (
|
||||
value: unknown,
|
||||
resultRef: string,
|
||||
context: RetrievalContext,
|
||||
): ResultReferenceRecord | null => {
|
||||
const data = extractLegacyRenderPayload(value);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = isRecord(value) ? value : {};
|
||||
const metadata = isRecord(root.metadata) ? root.metadata : {};
|
||||
const projectId = firstNonEmptyString(root.projectId, metadata.projectId);
|
||||
const createdAt =
|
||||
firstNonEmptyString(root.createdAt, metadata.createdAt) ?? new Date().toISOString();
|
||||
|
||||
return {
|
||||
resultRef,
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId ?? "",
|
||||
createdAt,
|
||||
data,
|
||||
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
||||
preview: buildPreview(data),
|
||||
projectId,
|
||||
projectKey: toProjectKey(projectId),
|
||||
schemaVersion: 1,
|
||||
sessionId: context.clientSessionId ?? resultRef,
|
||||
sizeBytes: estimateBytes(data),
|
||||
source: RESULT_REFERENCE_SOURCE.legacy,
|
||||
traceId: "legacy-render-ref",
|
||||
};
|
||||
};
|
||||
|
||||
const extractLegacyRenderPayload = (value: unknown) => {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const candidate = isRecord(value.data) ? value.data : value;
|
||||
if (!isRecord(candidate.node_area_map)) {
|
||||
return null;
|
||||
}
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const firstNonEmptyString = (...values: unknown[]) => {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isResultPreview = (value: unknown): value is ResultPreview =>
|
||||
isRecord(value) &&
|
||||
typeof value.count === "number" &&
|
||||
|
||||
+7
-2
@@ -12,12 +12,17 @@ import { logger } from "./logger.js";
|
||||
import { LearningOrchestrator } from "./learning/orchestrator.js";
|
||||
import { MemoryStore } from "./memory/store.js";
|
||||
import { ResultReferenceResolver } from "./results/resolver.js";
|
||||
import { ResultReferenceStore } from "./results/store.js";
|
||||
import {
|
||||
RESULT_REFERENCE_SOURCE,
|
||||
ResultReferenceStore,
|
||||
} from "./results/store.js";
|
||||
import { buildChatRouter } from "./routes/chat.js";
|
||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||
import { SessionRuntimeContextStore } from "./sessions/runtimeContextStore.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
// 这里集中组装 Agent 服务的运行期依赖,路由层只通过接口调用,便于测试时替换实现。
|
||||
const sessionBridge = new ChatSessionBridge(opencodeRuntime);
|
||||
const sessionMetadataStore = new SessionMetadataStore();
|
||||
const sessionUiStateStore = new SessionUiStateStore();
|
||||
@@ -190,7 +195,7 @@ app.post("/internal/tools/store-render-ref", async (req, res) => {
|
||||
projectId: context.projectId,
|
||||
projectKey: context.projectKey,
|
||||
sessionId: context.clientSessionId,
|
||||
source: "migration",
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: context.traceId,
|
||||
});
|
||||
res.json({
|
||||
|
||||
@@ -43,6 +43,9 @@ type SessionTranscriptContext = {
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
const DEFAULT_SEARCH_MAX_RESULTS = 8;
|
||||
const DEFAULT_SEARCH_MAX_QUERY_CHARS = 240;
|
||||
|
||||
export class SessionTranscriptStore {
|
||||
private readonly writeQueues = new Map<string, Promise<void>>();
|
||||
|
||||
@@ -62,6 +65,7 @@ export class SessionTranscriptStore {
|
||||
) {
|
||||
const key = this.filePath(context);
|
||||
return this.serializeWrite(key, async () => {
|
||||
// 同一会话的多次写入串行化,防止流式结束和 UI 同步同时写同一个 transcript。
|
||||
const transcript = (await this.readTranscript(context)) ?? {
|
||||
actorKey: context.actorKey,
|
||||
clientSessionId: context.clientSessionId,
|
||||
@@ -82,6 +86,7 @@ export class SessionTranscriptStore {
|
||||
lastTurn?.userMessage === userMessage &&
|
||||
lastTurn.assistantMessage === assistantMessage
|
||||
) {
|
||||
// 相同问答重复写入时只更新工具调用数量,避免刷新/重试造成 transcript 重复膨胀。
|
||||
lastTurn.toolCallCount = Math.max(lastTurn.toolCallCount, turn.toolCallCount);
|
||||
transcript.clientSessionId = context.clientSessionId ?? transcript.clientSessionId;
|
||||
transcript.sessionId = context.sessionId;
|
||||
@@ -129,6 +134,7 @@ export class SessionTranscriptStore {
|
||||
) {
|
||||
const sourceTranscript = await this.readTranscript(sourceContext);
|
||||
const timestamp = new Date().toISOString();
|
||||
// 分叉会话只复制用户选择保留的上下文,后续新分支拥有独立 transcript 文件。
|
||||
const nextTranscript: SessionTranscriptRecord = {
|
||||
actorKey: targetContext.actorKey,
|
||||
clientSessionId: targetContext.clientSessionId,
|
||||
@@ -144,9 +150,10 @@ export class SessionTranscriptStore {
|
||||
async search(
|
||||
context: Pick<SessionTranscriptContext, "actorKey" | "projectKey">,
|
||||
query: string,
|
||||
maxResults = config.SESSION_SEARCH_MAX_RESULTS,
|
||||
maxResults = DEFAULT_SEARCH_MAX_RESULTS,
|
||||
): Promise<SessionSearchHit[]> {
|
||||
const normalizedQuery = query.trim().toLowerCase().slice(0, config.SESSION_SEARCH_MAX_QUERY_CHARS);
|
||||
// 当前搜索是轻量本地文本匹配,按 actor/project 过滤后再计算简单相关性分数。
|
||||
const normalizedQuery = query.trim().toLowerCase().slice(0, DEFAULT_SEARCH_MAX_QUERY_CHARS);
|
||||
if (!normalizedQuery) {
|
||||
return [];
|
||||
}
|
||||
@@ -189,38 +196,7 @@ export class SessionTranscriptStore {
|
||||
}
|
||||
|
||||
private async readTranscript(context: SessionTranscriptContext) {
|
||||
const direct = await readJsonFile<SessionTranscriptRecord>(this.filePath(context));
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const clientSessionId = context.clientSessionId?.trim();
|
||||
if (!clientSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const files = await listJsonFiles(this.baseDir);
|
||||
const matches: SessionTranscriptRecord[] = [];
|
||||
for (const file of files) {
|
||||
const transcript = await readJsonFile<SessionTranscriptRecord>(file);
|
||||
if (!transcript) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
transcript.actorKey !== context.actorKey ||
|
||||
transcript.projectKey !== context.projectKey ||
|
||||
transcript.clientSessionId !== clientSessionId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
matches.push(transcript);
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return matches.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null;
|
||||
return await readJsonFile<SessionTranscriptRecord>(this.filePath(context));
|
||||
}
|
||||
|
||||
private filePath(context: SessionTranscriptContext) {
|
||||
|
||||
+3
-177
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -164,12 +164,12 @@ describe("ResultReferenceResolver", () => {
|
||||
projectId: "project-3",
|
||||
projectKey: "project-key-3",
|
||||
sessionId: "session-3",
|
||||
source: RESULT_REFERENCE_SOURCE.migration,
|
||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
||||
traceId: "trace-3",
|
||||
});
|
||||
|
||||
expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload);
|
||||
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.migration);
|
||||
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated);
|
||||
|
||||
const result = await resolver.getFullAuthorized(
|
||||
record.resultRef,
|
||||
@@ -195,178 +195,4 @@ describe("ResultReferenceResolver", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -19,63 +19,6 @@ describe("SessionTranscriptStore", () => {
|
||||
await rm(tempDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
|
||||
const recentTurns = await store.getRecentTurns(
|
||||
{
|
||||
actorKey: "actor-1",
|
||||
clientSessionId: "thread-1",
|
||||
projectKey: "project-1",
|
||||
sessionId: "thread-1",
|
||||
},
|
||||
5,
|
||||
);
|
||||
|
||||
expect(recentTurns).toHaveLength(1);
|
||||
expect(recentTurns[0]?.userMessage).toBe("帮我看一下当前异常。");
|
||||
|
||||
const transcript = await store.appendTurn(
|
||||
{
|
||||
actorKey: "actor-1",
|
||||
clientSessionId: "thread-1",
|
||||
projectKey: "project-1",
|
||||
sessionId: "thread-1",
|
||||
},
|
||||
{
|
||||
assistantMessage: "已经定位到 3 条疑似异常支路。",
|
||||
toolCallCount: 2,
|
||||
userMessage: "继续分析这些支路。",
|
||||
},
|
||||
);
|
||||
|
||||
expect(transcript.sessionId).toBe("thread-1");
|
||||
expect(transcript.turns).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("clones only the kept prefix when forking a thread", async () => {
|
||||
await store.appendTurn(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user