

























@@ -3,8 +3,8 @@ import os from "node:os";
33import path from "node:path";
44import { afterEach, beforeEach, describe, expect, it } from "vitest";
55import {
6-readSessionTranscriptTailLines,
76streamSessionTranscriptLines,
7+streamSessionTranscriptLinesReverse,
88} from "./transcript-stream.js";
991010// Regression coverage for #54296: the transcript readers must stay correct and
@@ -95,67 +95,100 @@ describe("streamSessionTranscriptLines", () => {
9595});
9696});
979798-describe("readSessionTranscriptTailLines", () => {
99-it("returns trimmed non-empty lines in reverse order for short files", async () => {
98+describe("streamSessionTranscriptLinesReverse", () => {
99+it("yields trimmed non-empty lines in reverse order for short files", async () => {
100100fs.writeFileSync(transcriptPath, "first\nsecond\nthird\n", "utf-8");
101101102-const lines = await readSessionTranscriptTailLines(transcriptPath);
102+const lines = await collect(streamSessionTranscriptLinesReverse(transcriptPath));
103103104104expect(lines).toEqual(["third", "second", "first"]);
105105});
106106107-it("returns undefined when the file cannot be opened", async () => {
108-const lines = await readSessionTranscriptTailLines(path.join(tempDir, "missing.jsonl"));
107+it("returns an empty iterator when the file cannot be opened", async () => {
108+const lines = await collect(
109+streamSessionTranscriptLinesReverse(path.join(tempDir, "missing.jsonl")),
110+);
109111110-expect(lines).toBeUndefined();
112+expect(lines).toEqual([]);
111113});
112114113-it("returns an empty array for an empty file", async () => {
115+it("returns an empty iterator for an empty file", async () => {
114116fs.writeFileSync(transcriptPath, "", "utf-8");
115117116-const lines = await readSessionTranscriptTailLines(transcriptPath);
118+const lines = await collect(streamSessionTranscriptLinesReverse(transcriptPath));
117119118120expect(lines).toEqual([]);
119121});
120122121-it("drops the leading partial line when the window does not start at byte zero", async () => {
122-// Build a file longer than the requested tail window. The first line of
123-// the slice we end up reading should be a suffix of an earlier line; the
124-// helper must discard it so callers do not see corrupt JSON.
125-const longPrefix = "x".repeat(2048);
126-const content = `${longPrefix}\nbeta\ngamma\n`;
127-fs.writeFileSync(transcriptPath, content, "utf-8");
123+it("preserves complete lines across chunk boundaries", async () => {
124+const longLine = "x".repeat(2048);
125+fs.writeFileSync(transcriptPath, `${longLine}\nbeta\ngamma\n`, "utf-8");
128126129-const lines = await readSessionTranscriptTailLines(transcriptPath, {
130-maxBytes: 16,
131-});
127+const lines = await collect(
128+streamSessionTranscriptLinesReverse(transcriptPath, {
129+chunkBytes: 1024,
130+}),
131+);
132132133-expect(lines).not.toContain(longPrefix);
134-expect(lines).toEqual(["gamma", "beta"]);
133+expect(lines).toEqual(["gamma", "beta", longLine]);
135134});
136135137-it("does not drop the first line when the window covers the entire file", async () => {
138-fs.writeFileSync(transcriptPath, "alpha\nbeta\ngamma\n", "utf-8");
136+it("preserves multibyte UTF-8 across chunk boundaries", async () => {
137+const firstLine = `${"a".repeat(1100)}🌊`;
138+const secondLine = `${"b".repeat(1100)}✅`;
139+fs.writeFileSync(transcriptPath, `${firstLine}\n${secondLine}\n`, "utf-8");
139140140-const lines = await readSessionTranscriptTailLines(transcriptPath, {
141-maxBytes: 64 * 1024,
142-});
141+const lines = await collect(
142+streamSessionTranscriptLinesReverse(transcriptPath, {
143+chunkBytes: 1024,
144+}),
145+);
143146144-expect(lines).toEqual(["gamma", "beta", "alpha"]);
147+expect(lines).toEqual([secondLine, firstLine]);
148+});
149+150+it("honours an abort signal between reverse lines", async () => {
151+fs.writeFileSync(transcriptPath, "one\ntwo\nthree\n", "utf-8");
152+const controller = new AbortController();
153+154+const out: string[] = [];
155+for await (const line of streamSessionTranscriptLinesReverse(transcriptPath, {
156+signal: controller.signal,
157+})) {
158+out.push(line);
159+if (line === "three") {
160+controller.abort();
161+}
162+}
163+164+expect(out).toEqual(["three"]);
145165});
146166147-it("clamps a sub-minimum maxBytes to the floor instead of returning nothing", async () => {
167+it("clamps a sub-minimum chunk size without dropping older lines", async () => {
148168fs.writeFileSync(transcriptPath, "alpha\nbeta\ngamma\n", "utf-8");
149169150-const lines = await readSessionTranscriptTailLines(transcriptPath, {
151-maxBytes: 16,
152-});
170+const lines = await collect(
171+streamSessionTranscriptLinesReverse(transcriptPath, {
172+chunkBytes: 16,
173+}),
174+);
153175154-// The full file is smaller than the 1 KiB floor, so we still read the
155-// whole file and return all three lines in reverse order.
156176expect(lines).toEqual(["gamma", "beta", "alpha"]);
157177});
158178179+it("does not emit a partial prefix until the full first line is available", async () => {
180+const firstLine = "prefix".repeat(400);
181+fs.writeFileSync(transcriptPath, `${firstLine}\nbeta\ngamma`, "utf-8");
182+183+const lines = await collect(
184+streamSessionTranscriptLinesReverse(transcriptPath, {
185+chunkBytes: 1024,
186+}),
187+);
188+189+expect(lines).toEqual(["gamma", "beta", firstLine]);
190+});
191+159192it("preserves JSONL line ordering so reverse scans hit the newest match first", async () => {
160193fs.writeFileSync(
161194transcriptPath,
@@ -167,10 +200,9 @@ describe("readSessionTranscriptTailLines", () => {
167200"utf-8",
168201);
169202170-const lines = await readSessionTranscriptTailLines(transcriptPath);
203+const lines = await collect(streamSessionTranscriptLinesReverse(transcriptPath));
204+const parsed = lines.map((line) => JSON.parse(line) as { id: string });
171205172-expect(lines).toBeDefined();
173-const parsed = lines!.map((line) => JSON.parse(line) as { id: string });
174206expect(parsed.map((entry) => entry.id)).toEqual(["third", "second", "first"]);
175207});
176208});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。