mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
196 lines
6.3 KiB
TypeScript
196 lines
6.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { CliRuntimeError } from "../utils/commandResult.js";
|
|
|
|
const { captureWebsiteMock } = vi.hoisted(() => ({
|
|
captureWebsiteMock: vi.fn(async (options: unknown) => {
|
|
if (typeof options === "object" && options !== null) {
|
|
const onPhase = Reflect.get(options, "onPhase");
|
|
if (typeof onPhase === "function") {
|
|
onPhase({
|
|
schema: "hyperframes.capture.phase.v1",
|
|
phase: "vision",
|
|
status: "degraded",
|
|
remainingMs: 0,
|
|
reason: "budget-exhausted",
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
ok: true,
|
|
projectDir: "/tmp/capture",
|
|
url: "https://example.com",
|
|
title: "Example",
|
|
extracted: {},
|
|
screenshots: [],
|
|
tokens: { sections: [], fonts: [] },
|
|
assets: [],
|
|
warnings: [],
|
|
};
|
|
}),
|
|
}));
|
|
|
|
vi.mock("../capture/index.js", () => ({ captureWebsite: captureWebsiteMock }));
|
|
|
|
import captureCommand from "./capture.js";
|
|
|
|
describe("capture command — vision control", () => {
|
|
afterEach(() => {
|
|
captureWebsiteMock.mockClear();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("declares --skip-vision as an opt-in boolean", () => {
|
|
const skipVisionArg = captureCommand.args
|
|
? Reflect.get(captureCommand.args, "skip-vision")
|
|
: undefined;
|
|
expect(skipVisionArg).toMatchObject({
|
|
type: "boolean",
|
|
default: false,
|
|
});
|
|
});
|
|
|
|
it("declares --capture-budget separately from navigation --timeout", () => {
|
|
const captureBudgetArg = captureCommand.args
|
|
? Reflect.get(captureCommand.args, "capture-budget")
|
|
: undefined;
|
|
expect(captureBudgetArg).toMatchObject({ type: "string" });
|
|
const description = captureBudgetArg?.description.toLowerCase();
|
|
expect(description).toContain("post-navigation");
|
|
expect(description).toContain("cooperative");
|
|
expect(description).toContain("not a hard wall-clock timeout");
|
|
expect(description).toContain("already-started native/core work");
|
|
expect(captureBudgetArg?.description).toContain("--timeout");
|
|
});
|
|
|
|
it("plumbs --skip-vision into capture options", async () => {
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
await captureCommand.run!({
|
|
args: {
|
|
url: "https://example.com",
|
|
output: "/tmp/hf-skip-vision-test",
|
|
"skip-assets": false,
|
|
"skip-vision": true,
|
|
json: true,
|
|
},
|
|
} as never);
|
|
|
|
expect(captureWebsiteMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ skipVision: true }),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
["1", 1],
|
|
["45000", 45_000],
|
|
])(
|
|
"plumbs positive integer --capture-budget %s into the post-navigation budget",
|
|
async (captureBudget, expectedBudget) => {
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
await captureCommand.run!({
|
|
args: {
|
|
url: "https://example.com",
|
|
output: "/tmp/hf-capture-budget-test",
|
|
"skip-assets": false,
|
|
"skip-vision": false,
|
|
"capture-budget": captureBudget,
|
|
json: true,
|
|
},
|
|
} as never);
|
|
|
|
expect(captureWebsiteMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ postNavigationBudgetMs: expectedBudget }),
|
|
undefined,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(["0", "-1", "0.5", "Infinity", "not-a-number"])(
|
|
"rejects invalid --capture-budget %s before capture starts",
|
|
async (captureBudget) => {
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
await expect(
|
|
captureCommand.run!({
|
|
args: {
|
|
url: "https://example.com",
|
|
output: "/tmp/hf-invalid-capture-budget-test",
|
|
"skip-assets": false,
|
|
"skip-vision": false,
|
|
"capture-budget": captureBudget,
|
|
json: true,
|
|
},
|
|
} as never),
|
|
).rejects.toBeInstanceOf(CliRuntimeError);
|
|
|
|
expect(captureWebsiteMock).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
|
|
it("emits a versioned phase record without the captured URL", async () => {
|
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
await captureCommand.run!({
|
|
args: {
|
|
url: "https://user:secret@example.com/private",
|
|
output: "/tmp/hf-phase-progress-test",
|
|
"skip-assets": false,
|
|
"skip-vision": false,
|
|
json: true,
|
|
},
|
|
} as never);
|
|
|
|
const line = error.mock.calls.find(
|
|
([value]) => typeof value === "string" && value.startsWith("HYPERFRAMES_CAPTURE_PHASE "),
|
|
)?.[0];
|
|
expect(typeof line).toBe("string");
|
|
if (typeof line !== "string") throw new Error("Expected capture phase diagnostic");
|
|
const event = JSON.parse(line.slice("HYPERFRAMES_CAPTURE_PHASE ".length));
|
|
expect(event).toEqual({
|
|
schema: "hyperframes.capture.phase.v1",
|
|
phase: "vision",
|
|
status: "degraded",
|
|
remainingMs: 0,
|
|
reason: "budget-exhausted",
|
|
});
|
|
expect(line).not.toContain("secret");
|
|
expect(line).not.toContain("/private");
|
|
});
|
|
|
|
it("keeps required capture failures nonzero", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "hf-capture-failure-"));
|
|
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
captureWebsiteMock.mockRejectedValueOnce(new Error("required extraction failed"));
|
|
|
|
try {
|
|
await expect(
|
|
captureCommand.run!({
|
|
args: {
|
|
url: "https://example.com",
|
|
output: dir,
|
|
"skip-assets": false,
|
|
"skip-vision": false,
|
|
json: true,
|
|
},
|
|
} as never),
|
|
).rejects.toBeInstanceOf(CliRuntimeError);
|
|
const payload = log.mock.calls.at(-1)?.[0];
|
|
expect(typeof payload).toBe("string");
|
|
if (typeof payload !== "string") throw new Error("Expected JSON failure payload");
|
|
expect(JSON.parse(payload)).toMatchObject({ ok: false, error: "required extraction failed" });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|