scripts/pr-shots.test.mjs
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { runPrShots } from "./pr-shots";
const prUrl = "https://github.com/multica-ai/multica/pull/123";
function setup(t, body = "## Problem\n\nExisting prose and formatting.\n") {
const directory = mkdtempSync(join(tmpdir(), "pr-shots-test-"));
t.after(() => rmSync(directory, { recursive: true, force: true }));
const state = { body, edits: 0, uploads: 0, version: "2.103.2", failAt: 0, failAfterSave: false, beforeRead: null };
const file = (name, content = name) => {
const path = join(directory, name);
writeFileSync(path, content);
return path;
};
const gh = (args) => {
const ok = (stdout = "") => ({ status: 0, stdout, stderr: "" });
if (args[0] === "--version") return ok(`gh version ${state.version} (test)`);
if (args[1] === "view") {
state.beforeRead?.();
return ok(JSON.stringify({ body: state.body, url: prUrl }));
}
assert.equal(args[1], "edit");
assert.equal(args[2], prUrl, "writes must use the resolved repository/PR");
state.edits++;
if (state.failAt === state.edits) return { status: 1, stdout: "", stderr: "upload denied" };
let body = readFileSync(args[args.indexOf("--body-file") + 1], "utf8");
if (args.includes("--attach")) {
const staged = args[args.indexOf("--attach") + 1];
assert.doesNotMatch(staged, /[#\[\]]/, "upload paths must not become gh alt text or Markdown syntax");
assert.ok(readFileSync(staged).length);
const url = `https://github.com/user-attachments/assets/attachment-${++state.uploads}`;
body = body.replace(``, url).split(staged).join(url);
}
state.body = body;
return state.failAfterSave ? { status: 1, stdout: prUrl, stderr: "connection lost after write" } : ok(prUrl);
};
const run = (...args) => {
let stdout = "", stderr = "";
const status = runPrShots(args, { gh, out: (s) => { stdout += s; }, log: (s) => { stderr += `${s}\n`; } });
return { status, stdout, stderr };
};
return { state, file, run, directory };
}
test("preserves prose, emits attachment Markdown, and reuses content after a rename", (t) => {
const { state, file, run, directory } = setup(t);
const original = state.body;
const image = file("before [login] #1.png");
const first = run("--repo", "multica-ai/multica", "123", image);
assert.equal(first.status, 0, first.stderr);
assert.ok(state.body.startsWith(original));
assert.match(first.stdout, /^!\[before \\\[login\\\] #1\.png\]\(https:\/\/github\.com\/user-attachments\/assets\//);
assert.doesNotMatch(first.stdout, /pr-shots:|pr-shots-test-/);
const renamed = join(directory, "renamed.png");
renameSync(image, renamed);
const second = run("123", renamed, renamed);
assert.equal(second.status, 0, second.stderr);
assert.equal(second.stdout, first.stdout);
assert.equal(state.uploads, 1);
assert.equal(state.edits, 1);
});
test("changed content at the same path is uploaded again", (t) => {
const { state, file, run } = setup(t);
const image = file("screen.png", "before");
assert.equal(run("123", image).status, 0);
writeFileSync(image, "after");
assert.equal(run("123", image).status, 0);
assert.equal(state.uploads, 2);
});
test("failed batch retains successes and retry uploads only missing files", (t) => {
const { state, file, run } = setup(t);
const files = [file("one.png"), file("two.png"), file("three.png")];
state.failAt = 2;
const failed = run("123", ...files);
assert.equal(failed.status, 1);
assert.match(failed.stdout, /attachment-1/);
assert.match(failed.stderr, /Failed two.png: upload denied/);
assert.match(failed.stderr, /Not attempted: three.png/);
assert.equal(state.uploads, 1);
state.failAt = 0;
const retried = run("123", ...files);
assert.equal(retried.status, 0, retried.stderr);
assert.equal(state.uploads, 3);
assert.equal((state.body.match(/pr-shots:sha256=/g) ?? []).length, 3);
});
test("nonzero gh exit after a successful write is read back and not reuploaded", (t) => {
const { state, file, run } = setup(t);
const image = file("screen.png");
state.failAfterSave = true;
const first = run("123", image);
assert.equal(first.status, 1);
assert.match(first.stdout, /attachment-1/);
assert.match(first.stderr, /Attachment saved, but gh exited 1/);
state.failAfterSave = false;
assert.equal(run("123", image).status, 0);
assert.equal(state.uploads, 1);
});
test("migration replaces the image in place, preserving captions and alt text", (t) => {
const old = "https://github.com/multica-ai/multica/raw/2492562/pr-10077/loader-frames.png";
const before = `Before caption.\n\n\n\nAfter caption.\n`;
const { state, file, run } = setup(t, before);
const image = file("screen.png");
const args = ["--replace", old, "123", image];
state.failAt = 1;
assert.equal(run(...args).status, 1);
assert.equal(state.body, before, "failed migration must retain the old working link");
state.failAt = 0;
const migrated = run(...args);
assert.equal(migrated.status, 0, migrated.stderr);
assert.match(migrated.stdout, /^!\[Original alt\]/);
assert.ok(state.body.startsWith("Before caption.\n\n"));
assert.ok(state.body.endsWith("\n\nAfter caption.\n"));
assert.ok(!state.body.includes(old));
assert.equal(run(...args).status, 0);
assert.equal(state.uploads, 1);
});
test("reads the latest body between uploads", (t) => {
const { state, file, run } = setup(t);
const one = file("one.png"), two = file("two.png");
state.beforeRead = () => {
if (state.uploads === 1 && !state.body.includes("New author note.")) state.body += "\nNew author note.\n";
};
assert.equal(run("123", one, two).status, 0);
assert.match(state.body, /New author note\./);
});
test("videos emit a player URL on its own line and are deduplicated", (t) => {
const { state, file, run } = setup(t);
const video = file("interaction.webm");
const first = run("123", video);
assert.equal(first.status, 0, first.stderr);
assert.match(first.stdout, /^https:\/\/github\.com\/user-attachments\/assets\/attachment-1\n\n$/);
assert.equal(run("123", video).stdout, first.stdout);
assert.equal(state.uploads, 1);
});
test("preflights all files and gh version before any writes", (t) => {
const { state, file, run, directory } = setup(t);
const image = file("screen.png");
assert.equal(run("123", image, join(directory, "missing.png")).status, 1);
assert.equal(run("123", image, file("secret.txt")).status, 1);
assert.equal(run("123", file("empty.png", "")).status, 1);
state.version = "2.99.0";
assert.match(run("123", image).stderr, /Upgrade GitHub CLI/);
assert.equal(state.edits, 0);
});
test("bad migration targets and unsupported arguments do not change the PR", (t) => {
const { state, file, run } = setup(t);
const image = file("screen.png");
assert.equal(run("--replace", "https://example.com/missing.png", "123", image).status, 1);
assert.equal(run("--replace", "https://example.com/video.mp4", "123", file("video.mp4")).status, 1);
assert.equal(run("--replace", "https://example.com/old.png", "123", image, image).status, 1);
assert.equal(run("--unexpected", "123", image).status, 1);
assert.equal(state.edits, 0);
});