Merge 9702782948 into 4583f88626
This commit is contained in:
commit
03e6186574
106
src/gateway/server-methods/chat.test.ts
Normal file
106
src/gateway/server-methods/chat.test.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { _testExports } from "./chat.js";
|
||||||
|
|
||||||
|
const { findTranscriptLeafId, appendAssistantTranscriptMessage } = _testExports;
|
||||||
|
|
||||||
|
describe("appendAssistantTranscriptMessage parentId chain", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "chat-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets parentId to previous message id when appending second message", () => {
|
||||||
|
const transcriptPath = path.join(tempDir, "transcript.jsonl");
|
||||||
|
const sessionId = "test-session";
|
||||||
|
|
||||||
|
// Append first message
|
||||||
|
const result1 = appendAssistantTranscriptMessage({
|
||||||
|
message: "First message",
|
||||||
|
sessionId,
|
||||||
|
storePath: undefined,
|
||||||
|
sessionFile: transcriptPath,
|
||||||
|
createIfMissing: true,
|
||||||
|
});
|
||||||
|
expect(result1.ok).toBe(true);
|
||||||
|
if (!result1.ok) throw new Error("First append failed");
|
||||||
|
|
||||||
|
const firstMessageId = result1.messageId;
|
||||||
|
|
||||||
|
// Append second message
|
||||||
|
const result2 = appendAssistantTranscriptMessage({
|
||||||
|
message: "Second message",
|
||||||
|
sessionId,
|
||||||
|
storePath: undefined,
|
||||||
|
sessionFile: transcriptPath,
|
||||||
|
});
|
||||||
|
expect(result2.ok).toBe(true);
|
||||||
|
|
||||||
|
// Read and parse the transcript
|
||||||
|
const lines = fs
|
||||||
|
.readFileSync(transcriptPath, "utf-8")
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.filter((l) => l.trim());
|
||||||
|
|
||||||
|
// Should have: header, first message, second message
|
||||||
|
expect(lines.length).toBe(3);
|
||||||
|
|
||||||
|
const secondMessageEntry = JSON.parse(lines[2]);
|
||||||
|
expect(secondMessageEntry.type).toBe("message");
|
||||||
|
|
||||||
|
// THE KEY ASSERTION: second message's parentId should be first message's id
|
||||||
|
// This fails on upstream main (parentId would be undefined/null)
|
||||||
|
// This passes on fix branch (parentId equals firstMessageId)
|
||||||
|
expect(secondMessageEntry.parentId).toBe(firstMessageId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findTranscriptLeafId", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "chat-test-"));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for non-existent file", () => {
|
||||||
|
const result = findTranscriptLeafId(path.join(tempDir, "missing.jsonl"));
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for file with only header", () => {
|
||||||
|
const transcriptPath = path.join(tempDir, "header-only.jsonl");
|
||||||
|
fs.writeFileSync(
|
||||||
|
transcriptPath,
|
||||||
|
JSON.stringify({ type: "session", id: "test", version: 1 }) + "\n",
|
||||||
|
);
|
||||||
|
const result = findTranscriptLeafId(transcriptPath);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns last message id", () => {
|
||||||
|
const transcriptPath = path.join(tempDir, "with-messages.jsonl");
|
||||||
|
const lines = [
|
||||||
|
JSON.stringify({ type: "session", id: "test", version: 1 }),
|
||||||
|
JSON.stringify({ type: "message", id: "msg-001", message: {} }),
|
||||||
|
JSON.stringify({ type: "message", id: "msg-002", message: {} }),
|
||||||
|
];
|
||||||
|
fs.writeFileSync(transcriptPath, lines.join("\n") + "\n");
|
||||||
|
|
||||||
|
const result = findTranscriptLeafId(transcriptPath);
|
||||||
|
expect(result).toBe("msg-002");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -82,6 +82,36 @@ function ensureTranscriptFile(params: { transcriptPath: string; sessionId: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the leaf (last message) ID from a transcript file.
|
||||||
|
* Returns null if file doesn't exist, is empty, or has no messages (header only).
|
||||||
|
*/
|
||||||
|
function findTranscriptLeafId(transcriptPath: string): string | null {
|
||||||
|
if (!fs.existsSync(transcriptPath)) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(transcriptPath, "utf-8");
|
||||||
|
const lines = content.split(/\r?\n/).filter((line) => line.trim());
|
||||||
|
if (lines.length === 0) return null;
|
||||||
|
|
||||||
|
// Walk backwards to find the last message entry with an id
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(lines[i]);
|
||||||
|
// Skip non-message entries (like session headers)
|
||||||
|
if (parsed?.type === "message" && typeof parsed?.id === "string") {
|
||||||
|
return parsed.id;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip malformed lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function appendAssistantTranscriptMessage(params: {
|
function appendAssistantTranscriptMessage(params: {
|
||||||
message: string;
|
message: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
@ -112,6 +142,9 @@ function appendAssistantTranscriptMessage(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the current leaf ID before we append
|
||||||
|
const parentId = findTranscriptLeafId(transcriptPath);
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const messageId = randomUUID().slice(0, 8);
|
const messageId = randomUUID().slice(0, 8);
|
||||||
const labelPrefix = params.label ? `[${params.label}]\n\n` : "";
|
const labelPrefix = params.label ? `[${params.label}]\n\n` : "";
|
||||||
@ -125,6 +158,7 @@ function appendAssistantTranscriptMessage(params: {
|
|||||||
const transcriptEntry = {
|
const transcriptEntry = {
|
||||||
type: "message",
|
type: "message",
|
||||||
id: messageId,
|
id: messageId,
|
||||||
|
parentId,
|
||||||
timestamp: new Date(now).toISOString(),
|
timestamp: new Date(now).toISOString(),
|
||||||
message: messageBody,
|
message: messageBody,
|
||||||
};
|
};
|
||||||
@ -679,3 +713,9 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
respond(true, { ok: true, messageId });
|
respond(true, { ok: true, messageId });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Exported for testing only
|
||||||
|
export const _testExports = {
|
||||||
|
findTranscriptLeafId,
|
||||||
|
appendAssistantTranscriptMessage,
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user