mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-06 17:44:31 +00:00
152 lines
6.4 KiB
TypeScript
152 lines
6.4 KiB
TypeScript
import { expect, test } from "bun:test"
|
|
import { mkdir, mkdtemp, rm } from "node:fs/promises"
|
|
import { WriteStream } from "node:fs"
|
|
import { once } from "node:events"
|
|
import { tmpdir } from "node:os"
|
|
import { join } from "node:path"
|
|
import { TextRenderable } from "@opentui/core"
|
|
import { createHarness, matches } from "../src/frontend/actions"
|
|
import { SimulationRenderer } from "../src/frontend/renderer"
|
|
import { Effect, Schema } from "effect"
|
|
import { Timeline, Pointer, type Event } from "../src/recording"
|
|
|
|
test("streams ANSI chunks into a versioned JSONL timeline", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "simulation-recording-"))
|
|
const path = join(directory, "nested", "timeline.jsonl")
|
|
|
|
try {
|
|
const timeline = await Timeline.create(path, 80, 24)
|
|
await new Promise<void>((resolve, reject) => {
|
|
timeline.write(Buffer.from("\u001b[2Jhello"), (error) => (error ? reject(error) : resolve()))
|
|
})
|
|
timeline.resize(100, 30)
|
|
expect(await timeline.finish()).toBe(path)
|
|
await new Promise<void>((resolve) => timeline.write(Buffer.from("ignored"), () => resolve()))
|
|
|
|
const events = (await Bun.file(path).text())
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line) as Event)
|
|
expect(events[0]).toEqual({ type: "header", version: 1, cols: 80, rows: 24, encoding: "base64" })
|
|
expect(events[1]?.type).toBe("output")
|
|
if (events[1]?.type !== "output") throw new Error("Missing output event")
|
|
expect(Buffer.from(events[1].data, "base64").toString()).toBe("\u001b[2Jhello")
|
|
expect(events[1].at_ms).toBeGreaterThanOrEqual(0)
|
|
expect(events[2]).toMatchObject({ type: "resize", cols: 100, rows: 30 })
|
|
expect(events.at(-1)).toMatchObject({ type: "output", data: "" })
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test("finishes the pointer sidecar on the output clock without changing the v1 timeline", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "simulation-pointer-recording-"))
|
|
try {
|
|
const path = join(directory, "timeline.jsonl")
|
|
const timeline = await Timeline.create(path, 80, 24)
|
|
timeline.write("before")
|
|
timeline.pointer("move", 12, 5)
|
|
timeline.pointer("click", 15, 6)
|
|
timeline.write("after")
|
|
const first = timeline.finish()
|
|
expect(timeline.finish()).toBe(first)
|
|
expect(await first).toBe(path)
|
|
timeline.pointer("move", 30, 10)
|
|
const pointers = (await Bun.file(join(directory, "timeline.pointers.jsonl")).text())
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => Schema.decodeUnknownSync(Schema.fromJsonString(Pointer))(line))
|
|
expect(pointers.map(({ action, x, y }) => ({ action, x, y }))).toEqual([
|
|
{ action: "move", x: 12, y: 5 },
|
|
{ action: "click", x: 15, y: 6 },
|
|
])
|
|
const output = (await Bun.file(path).text())
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line) as Event)
|
|
const firstOutput = output[1]
|
|
const lastOutput = output.at(-1)
|
|
if (firstOutput?.type !== "output" || lastOutput?.type !== "output") throw new Error("missing output")
|
|
expect(pointers[0]?.atMs).toBeGreaterThanOrEqual(firstOutput.at_ms)
|
|
expect(pointers[1]?.atMs).toBeLessThanOrEqual(lastOutput.at_ms)
|
|
expect(output.every((event) => ["header", "output", "resize"].includes(event.type))).toBe(true)
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test.each(["pointer", "output"])("joins both recording streams after an early %s failure", async (failed) => {
|
|
const directory = await mkdtemp(join(tmpdir(), "simulation-pointer-failure-"))
|
|
try {
|
|
const timeline = await Timeline.create(join(directory, "timeline.jsonl"), 80, 24)
|
|
if (failed === "pointer") await mkdir(join(directory, "timeline.pointers.jsonl"))
|
|
const error = new Promise<Error>((resolve) => timeline.once("error", resolve))
|
|
timeline.pointer("move", 10, 5)
|
|
const output: unknown = Reflect.get(timeline, "output")
|
|
const pointers: unknown = Reflect.get(timeline, "pointers")
|
|
if (!(output instanceof WriteStream) || !(pointers instanceof WriteStream)) throw new Error("missing owned streams")
|
|
if (failed === "output") {
|
|
if (pointers.pending) await once(pointers, "open")
|
|
output.destroy(new Error("output failed"))
|
|
}
|
|
const failure = await error
|
|
const finishing = timeline.finish()
|
|
expect(timeline.finish()).toBe(finishing)
|
|
await expect(finishing).rejects.toBe(failure)
|
|
expect(output.closed).toBe(true)
|
|
expect(pointers.closed).toBe(true)
|
|
expect(Reflect.get(output, "fd")).toBeNull()
|
|
expect(Reflect.get(pointers, "fd")).toBeNull()
|
|
timeline.pointer("move", 99, 99)
|
|
expect(timeline.finish()).toBe(finishing)
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test("captures native renderer output and finishes on destroy", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "simulation-renderer-recording-"))
|
|
const path = join(directory, "timeline.jsonl")
|
|
|
|
try {
|
|
await Effect.runPromise(
|
|
Effect.scoped(
|
|
Effect.gen(function* () {
|
|
const renderer = yield* SimulationRenderer.create({}, path)
|
|
yield* Effect.promise(() => SimulationRenderer.setupFor(renderer)?.renderOnce() ?? Promise.resolve())
|
|
renderer.destroy()
|
|
expect(yield* SimulationRenderer.finish(renderer)).toBe(path)
|
|
|
|
const events = (yield* Effect.promise(() => Bun.file(path).text()))
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line) as Event)
|
|
expect(events.some((event) => event.type === "output")).toBe(true)
|
|
}),
|
|
),
|
|
)
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
test("matches live screen text while recording", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "simulation-recording-matches-"))
|
|
const path = join(directory, "timeline.jsonl")
|
|
|
|
try {
|
|
await Effect.runPromise(
|
|
Effect.scoped(
|
|
Effect.gen(function* () {
|
|
const renderer = yield* SimulationRenderer.create({}, path)
|
|
renderer.root.add(new TextRenderable(renderer, { content: "recorded screen text" }))
|
|
yield* Effect.promise(() => SimulationRenderer.setupFor(renderer)?.renderOnce() ?? Promise.resolve())
|
|
|
|
expect(matches(createHarness(renderer), "recorded screen text")).toBe(true)
|
|
}),
|
|
),
|
|
)
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true })
|
|
}
|
|
})
|