mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
The scheduled Nightly had been red for over a week across nearly every job. This root-causes and fixes each one. All were pre-existing: missing CI provisioning, specs that drifted as the app grew, a job too heavy for its timeout, and a fuzz that was never configured for file-upload endpoints. None came from the recent security merge. - Coverage + Docker Container E2E: install tesseract and its language packs so the built-in Fast OCR tests stop throwing spawn ENOENT; gate two repo-file and release-workflow tests that cannot run inside the slimmed container image. - E2E (Full, Serial, Cross-Browser, Device Matrix): refresh specs that drifted behind the app (tool renames, the now admin-only Tools tab, dropped About copy, locator collisions scoped to the right region). One real product fix rode along: /config/auth was refetched six times per tool-page load, so cache it behind a single shared fetch, dropping the tool page from 13 to 8 API calls. - Extended Matrix + Fuzz: shard the integration suite four ways so the full format x tool matrix plus property fuzz fits its budget instead of overrunning the 90-minute ceiling every night. - Schemathesis: exclude the tools with bespoke handlers that process synchronously in-request (they hang the fuzz on adversarial input) and suppress Hypothesis's data-generation health checks, which fire because file-upload endpoints reject the fuzzer's random bytes. not_a_server_error still runs on every generated case (5000+ per run). - Stabilize two long-tail flakes: raise the avif matrix per-test cap from 240s to 600s, and assert toHaveCount(0) on the deleted user row so a transient success toast no longer trips a strict-mode violation. Verified end to end: the full Nightly workflow is green on this branch (all 14 jobs), and PR CI is green.
165 lines
6.0 KiB
TypeScript
165 lines
6.0 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { loadTranslations, SUPPORTED_LOCALES } from "@snapotter/shared";
|
|
import yaml from "js-yaml";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const root = join(import.meta.dirname, "../../..");
|
|
const localizedCodes = SUPPORTED_LOCALES.map((locale) => locale.code).filter(
|
|
(locale) => locale !== "en",
|
|
);
|
|
|
|
type OpenApiContract = {
|
|
components: {
|
|
schemas: {
|
|
FeatureNotInstalledError: {
|
|
properties: {
|
|
requestedQuality: { enum: string[]; description: string };
|
|
guidance: { description: string };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
paths: Record<
|
|
string,
|
|
{
|
|
post: {
|
|
requestBody: {
|
|
content: {
|
|
"multipart/form-data": {
|
|
schema: {
|
|
properties: {
|
|
file: { maxLength?: number };
|
|
settings: { description: string };
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|
|
>;
|
|
};
|
|
|
|
function readOpenApi(locale: string): OpenApiContract {
|
|
const suffix = locale === "en" ? "" : `.${locale}`;
|
|
return yaml.load(
|
|
readFileSync(join(root, `apps/api/src/openapi${suffix}.yaml`), "utf8"),
|
|
) as OpenApiContract;
|
|
}
|
|
|
|
describe("Korean OCR product contract", () => {
|
|
it("publishes the Fast incompatibility response in every OpenAPI locale", () => {
|
|
for (const locale of SUPPORTED_LOCALES.map((item) => item.code)) {
|
|
const spec = readOpenApi(locale);
|
|
const properties = spec.components.schemas.FeatureNotInstalledError.properties;
|
|
|
|
expect(properties.requestedQuality.enum, locale).toEqual(["fast", "balanced", "best"]);
|
|
expect(properties.requestedQuality.description, locale).toMatch(/Fast.*Korean/i);
|
|
expect(properties.guidance.description, locale).toMatch(/guidance/i);
|
|
|
|
for (const route of ["/api/v1/tools/image/ocr", "/api/v1/tools/pdf/ocr-pdf"]) {
|
|
const settings =
|
|
spec.paths[route].post.requestBody.content["multipart/form-data"].schema.properties
|
|
.settings.description;
|
|
expect(settings, `${locale} ${route}`).toContain("Korean never selects fast");
|
|
expect(settings, `${locale} ${route}`).toContain("legacy tesseract alias");
|
|
}
|
|
}
|
|
});
|
|
|
|
it("publishes the encoded-input ceiling on both OCR routes, not unrelated PDF tools", () => {
|
|
for (const locale of SUPPORTED_LOCALES.map((item) => item.code)) {
|
|
const spec = readOpenApi(locale);
|
|
const fileLimit = (route: string) =>
|
|
spec.paths[route].post.requestBody.content["multipart/form-data"].schema.properties.file
|
|
.maxLength;
|
|
|
|
expect(fileLimit("/api/v1/tools/image/ocr"), `${locale} image OCR`).toBe(536_870_912);
|
|
expect(fileLimit("/api/v1/tools/pdf/ocr-pdf"), `${locale} PDF OCR`).toBe(536_870_912);
|
|
expect(fileLimit("/api/v1/tools/pdf/flatten-pdf"), `${locale} flatten PDF`).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("describes Korean tier compatibility accurately on the landing page", () => {
|
|
const source = readFileSync(join(root, "apps/landing/src/data/tool-seo.ts"), "utf8");
|
|
|
|
expect(source).toContain("Korean requires the Balanced or Best tier");
|
|
expect(source).toContain("Fast returns an incompatibility error for Korean");
|
|
expect(source).not.toContain(
|
|
"The same language choices are available for the Fast, Balanced, and Best tiers.",
|
|
);
|
|
});
|
|
|
|
it("provides localized accessible guidance in all 21 app locales", async () => {
|
|
const english = (await loadTranslations("en")).toolSettings.ocr.fastKoreanUnsupported;
|
|
expect(english).toMatch(/Fast OCR.*Korean.*Accurate OCR pack.*Balanced or Best/);
|
|
|
|
for (const locale of localizedCodes) {
|
|
const message = (await loadTranslations(locale)).toolSettings.ocr.fastKoreanUnsupported;
|
|
expect(message, locale).toBeTruthy();
|
|
expect(message, locale).not.toBe(english);
|
|
}
|
|
});
|
|
|
|
it("documents the compatibility boundary in all localized OCR guides", () => {
|
|
const documents = [
|
|
"api/ai.md",
|
|
"guide/deployment.md",
|
|
"tools/image/ocr.md",
|
|
"tools/pdf/ocr-pdf.md",
|
|
];
|
|
|
|
for (const locale of localizedCodes) {
|
|
for (const document of documents) {
|
|
const content = readFileSync(join(root, "apps/docs", locale, document), "utf8");
|
|
expect(content, `${locale}/${document}`).toContain("<!-- korean-ocr-contract:start -->");
|
|
expect(content, `${locale}/${document}`).toContain("`fast-korean-unsupported`");
|
|
expect(content, `${locale}/${document}`).toContain("`FEATURE_INCOMPATIBLE`");
|
|
expect(content, `${locale}/${document}`).toContain("Linux amd64");
|
|
expect(content, `${locale}/${document}`).toContain("arm64");
|
|
expect(content, `${locale}/${document}`).toContain("NVIDIA");
|
|
}
|
|
}
|
|
});
|
|
|
|
it("publishes the measured 25 MiB Fast OCR footprint without stale 24/29 MiB copy", () => {
|
|
// Reads root DOCKERHUB.md, which .dockerignore strips from the shipped image;
|
|
// this repo-content contract runs in PR CI, not inside the container.
|
|
if (existsSync("/.dockerenv")) return;
|
|
const documents = [
|
|
"api/ai.md",
|
|
"guide/deployment.md",
|
|
"tools/image/ocr.md",
|
|
"tools/pdf/ocr-pdf.md",
|
|
];
|
|
const files = [
|
|
"README.md",
|
|
"DOCKERHUB.md",
|
|
"llms.txt",
|
|
"apps/landing/public/llms.txt",
|
|
...documents.map((document) => join("apps/docs", document)),
|
|
...localizedCodes.flatMap((locale) =>
|
|
documents.map((document) => join("apps/docs", locale, document)),
|
|
),
|
|
];
|
|
|
|
expect(files).toHaveLength(88);
|
|
for (const file of files) {
|
|
const footprintLines = readFileSync(join(root, file), "utf8")
|
|
.split("\n")
|
|
.filter(
|
|
(line) =>
|
|
/OCR|Tesseract|`fast`/i.test(line) &&
|
|
line.includes("MiB") &&
|
|
/\b(?:24|25|29)\b/.test(line),
|
|
);
|
|
|
|
expect(footprintLines, file).toHaveLength(1);
|
|
expect(footprintLines[0], file).toMatch(/\b25\b/);
|
|
expect(footprintLines[0], file).not.toMatch(/\b24\b/);
|
|
expect(footprintLines[0], file).not.toMatch(/\b29\b/);
|
|
}
|
|
});
|
|
});
|