403 lines
13 KiB
TypeScript
403 lines
13 KiB
TypeScript
import { describe, expect, test, beforeAll, afterAll } from "bun:test"
|
|
import { promises as fs } from "fs"
|
|
import path from "path"
|
|
import os from "os"
|
|
|
|
const SCRIPT = path.join(
|
|
process.cwd(),
|
|
"plugins",
|
|
"compound-engineering",
|
|
"skills",
|
|
"ce-demo-reel",
|
|
"scripts",
|
|
"capture-demo.py",
|
|
)
|
|
|
|
async function run(
|
|
...args: string[]
|
|
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
const proc = Bun.spawn(["python3", SCRIPT, ...args], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
})
|
|
const exitCode = await proc.exited
|
|
const stdout = await new Response(proc.stdout).text()
|
|
const stderr = await new Response(proc.stderr).text()
|
|
return { exitCode, stdout, stderr }
|
|
}
|
|
|
|
/** Create a minimal valid PNG (1x1 pixel, solid color). */
|
|
function createTestPng(color: [number, number, number]): Buffer {
|
|
const [r, g, b] = color
|
|
|
|
// Raw RGB pixel data: 1 row, filter byte 0, then RGB
|
|
const rawData = Buffer.from([0, r, g, b])
|
|
|
|
// Compress with zlib
|
|
const compressed = Bun.deflateSync(rawData, { level: 0 })
|
|
const cmf = 0x78
|
|
const flg = 0x01
|
|
let s1 = 1
|
|
let s2 = 0
|
|
for (const byte of rawData) {
|
|
s1 = (s1 + byte) % 65521
|
|
s2 = (s2 + s1) % 65521
|
|
}
|
|
const adler32 = Buffer.alloc(4)
|
|
adler32.writeUInt32BE((s2 << 16) | s1)
|
|
const zlibData = Buffer.concat([Buffer.from([cmf, flg]), compressed, adler32])
|
|
|
|
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
|
|
|
function chunk(type: string, data: Buffer): Buffer {
|
|
const len = Buffer.alloc(4)
|
|
len.writeUInt32BE(data.length)
|
|
const typeB = Buffer.from(type, "ascii")
|
|
const body = Buffer.concat([typeB, data])
|
|
const crc = crc32(body)
|
|
const crcB = Buffer.alloc(4)
|
|
crcB.writeUInt32BE(crc >>> 0)
|
|
return Buffer.concat([len, body, crcB])
|
|
}
|
|
|
|
// IHDR: 1x1, 8-bit RGB (color type 2)
|
|
const ihdr = Buffer.alloc(13)
|
|
ihdr.writeUInt32BE(1, 0)
|
|
ihdr.writeUInt32BE(1, 4)
|
|
ihdr[8] = 8 // bit depth
|
|
ihdr[9] = 2 // color type: RGB
|
|
ihdr[10] = 0
|
|
ihdr[11] = 0
|
|
ihdr[12] = 0
|
|
|
|
return Buffer.concat([
|
|
signature,
|
|
chunk("IHDR", ihdr),
|
|
chunk("IDAT", zlibData),
|
|
chunk("IEND", Buffer.alloc(0)),
|
|
])
|
|
}
|
|
|
|
function crc32(data: Buffer): number {
|
|
let crc = 0xffffffff
|
|
for (const byte of data) {
|
|
crc ^= byte
|
|
for (let j = 0; j < 8; j++) {
|
|
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1
|
|
}
|
|
}
|
|
return (crc ^ 0xffffffff) >>> 0
|
|
}
|
|
|
|
// --- Preflight ---
|
|
|
|
describe("capture-evidence.py", () => {
|
|
describe("preflight", () => {
|
|
test("returns JSON with tool availability", async () => {
|
|
const { exitCode, stdout } = await run("preflight")
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result).toHaveProperty("agent_browser")
|
|
expect(result).toHaveProperty("vhs")
|
|
expect(result).toHaveProperty("silicon")
|
|
expect(result).toHaveProperty("ffmpeg")
|
|
expect(result).toHaveProperty("ffprobe")
|
|
expect(typeof result.ffmpeg).toBe("boolean")
|
|
})
|
|
})
|
|
|
|
// --- Detect ---
|
|
|
|
describe("detect", () => {
|
|
let tmpDir: string
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "evidence-detect-"))
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
test("detects web-app from package.json with react", async () => {
|
|
const dir = path.join(tmpDir, "webapp")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(
|
|
path.join(dir, "package.json"),
|
|
JSON.stringify({ dependencies: { react: "^18.0.0" } }),
|
|
)
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("web-app")
|
|
})
|
|
|
|
test("detects cli-tool from package.json with bin field", async () => {
|
|
const dir = path.join(tmpDir, "clitool")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(
|
|
path.join(dir, "package.json"),
|
|
JSON.stringify({ bin: { mycli: "./cli.js" } }),
|
|
)
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("cli-tool")
|
|
})
|
|
|
|
test("detects desktop-app from electron dependency", async () => {
|
|
const dir = path.join(tmpDir, "electron")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(
|
|
path.join(dir, "package.json"),
|
|
JSON.stringify({ devDependencies: { electron: "^28.0.0", react: "^18.0.0" } }),
|
|
)
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("desktop-app")
|
|
})
|
|
|
|
test("detects library when manifest exists but no web/CLI signals", async () => {
|
|
const dir = path.join(tmpDir, "lib")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(
|
|
path.join(dir, "package.json"),
|
|
JSON.stringify({ name: "my-utils", version: "1.0.0" }),
|
|
)
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("library")
|
|
})
|
|
|
|
test("detects text-only when no manifest exists", async () => {
|
|
const dir = path.join(tmpDir, "textonly")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(path.join(dir, "README.md"), "# Hello")
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("text-only")
|
|
})
|
|
|
|
test("electron takes priority over web-app", async () => {
|
|
const dir = path.join(tmpDir, "electron-react")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(
|
|
path.join(dir, "package.json"),
|
|
JSON.stringify({ dependencies: { react: "^18.0.0" }, devDependencies: { electron: "^28.0.0" } }),
|
|
)
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("desktop-app")
|
|
})
|
|
|
|
test("detects web-app from Gemfile with rails", async () => {
|
|
const dir = path.join(tmpDir, "rails")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(path.join(dir, "Gemfile"), 'gem "rails", "~> 7.0"')
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("web-app")
|
|
})
|
|
|
|
test("detects cli-tool from go.mod with cmd/ directory", async () => {
|
|
const dir = path.join(tmpDir, "gocli")
|
|
await fs.mkdir(dir)
|
|
await fs.writeFile(path.join(dir, "go.mod"), "module example.com/mycli\n\ngo 1.21")
|
|
await fs.mkdir(path.join(dir, "cmd"))
|
|
const { exitCode, stdout } = await run("detect", "--repo-root", dir)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.type).toBe("cli-tool")
|
|
})
|
|
})
|
|
|
|
// --- Recommend ---
|
|
|
|
describe("recommend", () => {
|
|
const allTools = '{"agent_browser":true,"vhs":true,"silicon":true,"ffmpeg":true,"ffprobe":true}'
|
|
const noTools = '{"agent_browser":false,"vhs":false,"silicon":false,"ffmpeg":false,"ffprobe":false}'
|
|
|
|
test("web-app with browser + ffmpeg recommends browser-reel", async () => {
|
|
const { exitCode, stdout } = await run(
|
|
"recommend", "--project-type", "web-app", "--change-type", "states", "--tools", allTools,
|
|
)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.recommended).toBe("browser-reel")
|
|
})
|
|
|
|
test("cli-tool with motion + vhs recommends terminal-recording", async () => {
|
|
const { exitCode, stdout } = await run(
|
|
"recommend", "--project-type", "cli-tool", "--change-type", "motion", "--tools", allTools,
|
|
)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.recommended).toBe("terminal-recording")
|
|
})
|
|
|
|
test("cli-tool with states + silicon recommends screenshot-reel", async () => {
|
|
const tools = '{"agent_browser":false,"vhs":false,"silicon":true,"ffmpeg":true,"ffprobe":true}'
|
|
const { exitCode, stdout } = await run(
|
|
"recommend", "--project-type", "cli-tool", "--change-type", "states", "--tools", tools,
|
|
)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.recommended).toBe("screenshot-reel")
|
|
})
|
|
|
|
test("library always recommends static-screenshots", async () => {
|
|
const { exitCode, stdout } = await run(
|
|
"recommend", "--project-type", "library", "--change-type", "states", "--tools", allTools,
|
|
)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.recommended).toBe("static-screenshots")
|
|
})
|
|
|
|
test("no tools always falls back to static-screenshots", async () => {
|
|
const { exitCode, stdout } = await run(
|
|
"recommend", "--project-type", "cli-tool", "--change-type", "motion", "--tools", noTools,
|
|
)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.recommended).toBe("static-screenshots")
|
|
})
|
|
|
|
test("available list includes only tiers with tools present", async () => {
|
|
const tools = '{"agent_browser":false,"vhs":true,"silicon":false,"ffmpeg":true,"ffprobe":true}'
|
|
const { exitCode, stdout } = await run(
|
|
"recommend", "--project-type", "cli-tool", "--change-type", "motion", "--tools", tools,
|
|
)
|
|
expect(exitCode).toBe(0)
|
|
const result = JSON.parse(stdout.trim())
|
|
expect(result.available).toContain("terminal-recording")
|
|
expect(result.available).toContain("static-screenshots")
|
|
expect(result.available).not.toContain("browser-reel")
|
|
expect(result.available).not.toContain("screenshot-reel")
|
|
})
|
|
})
|
|
|
|
// --- Stitch arg validation ---
|
|
|
|
describe("stitch arg validation", () => {
|
|
test("stitch with no args fails", async () => {
|
|
const { exitCode, stderr } = await run("stitch")
|
|
expect(exitCode).not.toBe(0)
|
|
})
|
|
|
|
test("stitch fails on missing frame file", async () => {
|
|
const { exitCode, stderr } = await run(
|
|
"stitch", "out.gif", "/tmp/nonexistent-frame-abc123.png",
|
|
)
|
|
expect(exitCode).toBe(1)
|
|
expect(stderr).toContain("Frame not found")
|
|
})
|
|
|
|
test("upload fails on missing file", async () => {
|
|
const { exitCode, stderr } = await run(
|
|
"upload", "/tmp/nonexistent-file-abc123.gif",
|
|
)
|
|
expect(exitCode).toBe(1)
|
|
expect(stderr).toContain("File not found")
|
|
})
|
|
})
|
|
|
|
// --- Stitch integration (requires ffmpeg) ---
|
|
|
|
describe("stitch integration", () => {
|
|
let tmpDir: string
|
|
let hasFFmpeg: boolean
|
|
|
|
beforeAll(async () => {
|
|
const proc = Bun.spawn(["which", "ffmpeg"], {
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
})
|
|
hasFFmpeg = (await proc.exited) === 0
|
|
|
|
if (!hasFFmpeg) return
|
|
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "evidence-test-"))
|
|
|
|
const red = createTestPng([255, 0, 0])
|
|
const green = createTestPng([0, 255, 0])
|
|
const blue = createTestPng([0, 0, 255])
|
|
|
|
await fs.writeFile(path.join(tmpDir, "frame1.png"), red)
|
|
await fs.writeFile(path.join(tmpDir, "frame2.png"), green)
|
|
await fs.writeFile(path.join(tmpDir, "frame3.png"), blue)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true })
|
|
})
|
|
|
|
test("stitches frames into a GIF", async () => {
|
|
if (!hasFFmpeg) {
|
|
console.log("Skipping: ffmpeg not available")
|
|
return
|
|
}
|
|
|
|
const output = path.join(tmpDir, "output.gif")
|
|
const { exitCode, stdout } = await run(
|
|
"stitch", "--duration", "0.5", output,
|
|
path.join(tmpDir, "frame1.png"),
|
|
path.join(tmpDir, "frame2.png"),
|
|
)
|
|
|
|
expect(exitCode).toBe(0)
|
|
expect(stdout).toContain("Stitching 2 frames")
|
|
expect(stdout).toContain("Created:")
|
|
|
|
const stat = await fs.stat(output)
|
|
expect(stat.size).toBeGreaterThan(0)
|
|
|
|
const header = Buffer.alloc(6)
|
|
const fh = await fs.open(output, "r")
|
|
await fh.read(header, 0, 6)
|
|
await fh.close()
|
|
expect(header.toString("ascii").startsWith("GIF")).toBe(true)
|
|
})
|
|
|
|
test("stitches 3 frames into a GIF", async () => {
|
|
if (!hasFFmpeg) {
|
|
console.log("Skipping: ffmpeg not available")
|
|
return
|
|
}
|
|
|
|
const output = path.join(tmpDir, "output3.gif")
|
|
const { exitCode, stdout } = await run(
|
|
"stitch", "--duration", "0.5", output,
|
|
path.join(tmpDir, "frame1.png"),
|
|
path.join(tmpDir, "frame2.png"),
|
|
path.join(tmpDir, "frame3.png"),
|
|
)
|
|
|
|
expect(exitCode).toBe(0)
|
|
expect(stdout).toContain("Stitching 3 frames")
|
|
})
|
|
|
|
test("default duration is used when --duration not specified", async () => {
|
|
if (!hasFFmpeg) {
|
|
console.log("Skipping: ffmpeg not available")
|
|
return
|
|
}
|
|
|
|
const output = path.join(tmpDir, "output-default-dur.gif")
|
|
const { exitCode, stdout } = await run(
|
|
"stitch", output,
|
|
path.join(tmpDir, "frame1.png"),
|
|
path.join(tmpDir, "frame2.png"),
|
|
)
|
|
|
|
expect(exitCode).toBe(0)
|
|
expect(stdout).toContain("Created:")
|
|
})
|
|
})
|
|
})
|