mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 23:29:50 +00:00
fix(cli): preserve capture failure diagnostics
This commit is contained in:
@@ -309,6 +309,35 @@ describe("captionImagesWithGemini — OpenRouter provider", () => {
|
|||||||
expect(fetchMock).not.toHaveBeenCalled();
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("classifies non-provider pipeline failures without leaking the local path", async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), "hf-caption-no-assets-"));
|
||||||
|
dirs.push(dir);
|
||||||
|
vi.stubEnv("OPENROUTER_API_KEY", "or-unused-key");
|
||||||
|
|
||||||
|
const warnings: string[] = [];
|
||||||
|
let outcome: VisionCaptionOutcome | undefined;
|
||||||
|
const captions = await captionImagesWithGemini(dir, () => {}, warnings, {
|
||||||
|
onOutcome: (value) => {
|
||||||
|
outcome = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(captions).toEqual({});
|
||||||
|
expect(warnings).toEqual(["OpenRouter captioning failed internally; captions omitted."]);
|
||||||
|
expect(warnings.join(" ")).not.toContain(dir);
|
||||||
|
expect(outcome).toEqual({
|
||||||
|
timedOutRequests: 0,
|
||||||
|
failedRequests: 0,
|
||||||
|
budgetExhausted: false,
|
||||||
|
internalError: true,
|
||||||
|
});
|
||||||
|
if (!outcome) throw new Error("Expected vision caption outcome");
|
||||||
|
expect(resolveVisionPhaseCompletion(outcome, 10_000)).toEqual({
|
||||||
|
status: "degraded",
|
||||||
|
reason: "internal-error",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("skips captioning entirely when no provider key is present", async () => {
|
it("skips captioning entirely when no provider key is present", async () => {
|
||||||
const dir = makeProjectWithImages();
|
const dir = makeProjectWithImages();
|
||||||
dirs.push(dir);
|
dirs.push(dir);
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface VisionCaptionOutcome {
|
|||||||
timedOutRequests: number;
|
timedOutRequests: number;
|
||||||
failedRequests: number;
|
failedRequests: number;
|
||||||
budgetExhausted: boolean;
|
budgetExhausted: boolean;
|
||||||
|
/** Failure outside provider request handling (filesystem, module, or programming error). */
|
||||||
|
internalError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VisionCaptionOptions {
|
interface VisionCaptionOptions {
|
||||||
@@ -36,11 +38,14 @@ export function resolveVisionPhaseCompletion(
|
|||||||
| { status: "completed" }
|
| { status: "completed" }
|
||||||
| {
|
| {
|
||||||
status: "degraded";
|
status: "degraded";
|
||||||
reason: "budget-exhausted" | "request-timeout" | "provider-error";
|
reason: "budget-exhausted" | "request-timeout" | "provider-error" | "internal-error";
|
||||||
} {
|
} {
|
||||||
if (outcome.budgetExhausted || remainingMs <= 0) {
|
if (outcome.budgetExhausted || remainingMs <= 0) {
|
||||||
return { status: "degraded", reason: "budget-exhausted" };
|
return { status: "degraded", reason: "budget-exhausted" };
|
||||||
}
|
}
|
||||||
|
if (outcome.internalError) {
|
||||||
|
return { status: "degraded", reason: "internal-error" };
|
||||||
|
}
|
||||||
if (outcome.timedOutRequests > 0) {
|
if (outcome.timedOutRequests > 0) {
|
||||||
return { status: "degraded", reason: "request-timeout" };
|
return { status: "degraded", reason: "request-timeout" };
|
||||||
}
|
}
|
||||||
@@ -245,12 +250,15 @@ export async function captionImagesWithGemini(
|
|||||||
let timedOutCount = 0;
|
let timedOutCount = 0;
|
||||||
let failedRequestCount = 0;
|
let failedRequestCount = 0;
|
||||||
let budgetExhausted = false;
|
let budgetExhausted = false;
|
||||||
|
let internalError = false;
|
||||||
const reportOutcome = (): void => {
|
const reportOutcome = (): void => {
|
||||||
options.onOutcome?.({
|
const outcome: VisionCaptionOutcome = {
|
||||||
timedOutRequests: timedOutCount,
|
timedOutRequests: timedOutCount,
|
||||||
failedRequests: failedRequestCount,
|
failedRequests: failedRequestCount,
|
||||||
budgetExhausted,
|
budgetExhausted,
|
||||||
});
|
};
|
||||||
|
if (internalError) outcome.internalError = true;
|
||||||
|
options.onOutcome?.(outcome);
|
||||||
};
|
};
|
||||||
if (options.skipVision) {
|
if (options.skipVision) {
|
||||||
reportOutcome();
|
reportOutcome();
|
||||||
@@ -546,8 +554,8 @@ export async function captionImagesWithGemini(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
failedRequestCount = Math.max(1, failedRequestCount);
|
internalError = true;
|
||||||
warnings.push(`${providerName} captioning failed; captions omitted.`);
|
warnings.push(`${providerName} captioning failed internally; captions omitted.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
reportOutcome();
|
reportOutcome();
|
||||||
|
|||||||
@@ -267,7 +267,10 @@ export async function captureWebsite(
|
|||||||
httpStatus: navigationResponse?.status() ?? null,
|
httpStatus: navigationResponse?.status() ?? null,
|
||||||
...pageContentCheck,
|
...pageContentCheck,
|
||||||
});
|
});
|
||||||
if (blockedReason) throw new Error(blockedReason);
|
if (blockedReason) {
|
||||||
|
phase("navigation", "degraded", "blocked");
|
||||||
|
throw new Error(blockedReason);
|
||||||
|
}
|
||||||
|
|
||||||
phase("navigation", "completed");
|
phase("navigation", "completed");
|
||||||
phase("core-extraction", "started");
|
phase("core-extraction", "started");
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ describe("detectBlockedPage", () => {
|
|||||||
hasChallengeElement: false,
|
hasChallengeElement: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "classic Cloudflare interstitial",
|
||||||
|
evidence: {
|
||||||
|
httpStatus: 503,
|
||||||
|
title: "Attention Required! | Cloudflare",
|
||||||
|
textLength: 120,
|
||||||
|
bodyChildCount: 3,
|
||||||
|
hasChallengeElement: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
])("rejects a minimal protection page: $label", ({ evidence }) => {
|
])("rejects a minimal protection page: $label", ({ evidence }) => {
|
||||||
expect(detectBlockedPage(evidence)).toMatch(/capture blocked/i);
|
expect(detectBlockedPage(evidence)).toMatch(/capture blocked/i);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function detectBlockedPage(evidence: PageLoadEvidence): string | undefine
|
|||||||
const hasBlockedStatus =
|
const hasBlockedStatus =
|
||||||
evidence.httpStatus === 401 || evidence.httpStatus === 403 || evidence.httpStatus === 429;
|
evidence.httpStatus === 401 || evidence.httpStatus === 403 || evidence.httpStatus === 429;
|
||||||
const hasBlockedTitle =
|
const hasBlockedTitle =
|
||||||
/^(?:(?:error\s*)?(?:401|403|429)(?:\s*(?:[-:—]\s*)?(?:forbidden|unauthorized|access denied|too many requests))?|forbidden|access denied|attention required|just a moment(?:\.{3})?)(?:\s*[|—-]\s*(?:cloudflare|sucuri website firewall))?$/i.test(
|
/^(?:(?:error\s*)?(?:401|403|429)(?:\s*(?:[-:—]\s*)?(?:forbidden|unauthorized|access denied|too many requests))?|forbidden|access denied|attention required!?|just a moment(?:\.{3})?)(?:\s*[|—-]\s*(?:cloudflare|sucuri website firewall))?$/i.test(
|
||||||
evidence.title.trim(),
|
evidence.title.trim(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ export interface CapturePhaseProgress {
|
|||||||
status: "started" | "completed" | "degraded";
|
status: "started" | "completed" | "degraded";
|
||||||
/** Null before the post-navigation budget begins. */
|
/** Null before the post-navigation budget begins. */
|
||||||
remainingMs: number | null;
|
remainingMs: number | null;
|
||||||
reason?: "budget-exhausted" | "disabled" | "request-timeout" | "provider-error";
|
reason?:
|
||||||
|
| "budget-exhausted"
|
||||||
|
| "disabled"
|
||||||
|
| "request-timeout"
|
||||||
|
| "provider-error"
|
||||||
|
| "internal-error"
|
||||||
|
| "blocked";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CaptureOptions {
|
export interface CaptureOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user