Fix MVP audit evidence pipeline

This commit is contained in:
2026-06-08 08:33:15 +02:00
parent a45b92ea0a
commit ff18fc202e
16 changed files with 771 additions and 52 deletions

View File

@@ -1,12 +1,11 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import test from "node:test";
import {
buildAuditEvidenceInput,
type SkillRegistryEntryEvidence,
} from "../lib/ai/audit-evidence";
import { LOCAL_AUDIT_SKILL_REGISTRY_SOURCE } from "../lib/ai/local-audit-skill-registry";
import { parseSkillsRegistry } from "../lib/skills-registry";
const SAMPLE_SKILL_REGISTRY: SkillRegistryEntryEvidence[] = [
@@ -340,11 +339,7 @@ test("buildAuditEvidenceInput selects deterministic skills and supports design/u
});
test("buildAuditEvidenceInput prioritizes local-audit v3 skills before cap", () => {
const source = readFileSync(
join(process.cwd(), "v2_elemente", "skills.md"),
"utf8",
);
const skillRegistry = parseSkillsRegistry(source);
const skillRegistry = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE);
assert.equal(
skillRegistry.some((skill) => skill.id === "visual-design" && !skill.category),
@@ -448,11 +443,7 @@ test("buildAuditEvidenceInput prioritizes local-audit v3 skills before cap", ()
});
test("buildAuditEvidenceInput gates v3 skills when declared inputs are missing", () => {
const source = readFileSync(
join(process.cwd(), "v2_elemente", "skills.md"),
"utf8",
);
const skillRegistry = parseSkillsRegistry(source);
const skillRegistry = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE);
const actual = buildAuditEvidenceInput({
lead: {

View File

@@ -182,16 +182,21 @@ test("action calls generateObject with required schemas", () => {
}
});
test("action loads v3 skill registry from v2 source for evidence input", () => {
test("action loads v3 skill registry from bundled MVP source for evidence input", () => {
assert.equal(
hasPattern(actionSource, /import\s*{[\s\S]*loadSkillsRegistry[\s\S]*}\s*from\s*["']\.\.\/lib\/skills-registry["']/),
hasPattern(actionSource, /import\s*{[\s\S]*loadLocalAuditSkillRegistry[\s\S]*}\s*from\s*["']\.\.\/lib\/ai\/local-audit-skill-registry["']/),
true,
"Action should import loadSkillsRegistry from the shared registry parser.",
"Action should import the bundled MVP skill registry loader.",
);
assert.equal(
hasPattern(actionSource, /loadSkillsRegistry\(\s*(?:join\()?[\s\S]*v2_elemente[\s\S]*skills\.md[\s\S]*\)/),
hasPattern(actionSource, /loadLocalAuditSkillRegistry\(\s*\)/),
true,
"Action should load the v3 registry from v2_elemente/skills.md.",
"Action should load the v3 registry from a bundled MVP module.",
);
assert.doesNotMatch(
actionSource,
/v2_elemente|process\.cwd\(\)|loadSkillsRegistry\(|node:path/,
"Action should not read v2 reference files or filesystem paths at runtime.",
);
assert.equal(
hasPattern(actionSource, /skillRegistry:\s*\[\s*\]/),

View File

@@ -224,6 +224,50 @@ test("persistAuditGenerationResult inserts into auditGenerations", () => {
);
});
test("getAuditGenerationEvidence loads latest successful website enrichment evidence by lead", () => {
const evidenceSource = extractExportSource("getAuditGenerationEvidence");
assert.equal(
hasPattern(
evidenceSource,
/query\("agentRuns"\)[\s\S]*withIndex\("by_type_and_status_and_leadId"[\s\S]*eq\("type",\s*"website_enrichment"\)[\s\S]*eq\("status",\s*"succeeded"\)[\s\S]*eq\("leadId",\s*lead\._id\)[\s\S]*order\("desc"\)[\s\S]*take\(1\)/,
),
true,
"Evidence query should locate the latest successful website_enrichment run for the same lead.",
);
assert.equal(
hasPattern(
evidenceSource,
/const\s+enrichmentEvidenceRunId\s*=\s*latestSuccessfulEnrichmentRun\[0\]\?\._id\s*\?\?\s*args\.runId/,
),
true,
"Evidence query should fall back to the audit run only when no enrichment run exists.",
);
for (const table of [
"websiteCrawlPages",
"websiteTechnicalChecks",
]) {
assert.equal(
hasPattern(
evidenceSource,
new RegExp(
`query\\("${table}"\\)[\\s\\S]*withIndex\\("by_runId"[\\s\\S]*eq\\("runId",\\s*enrichmentEvidenceRunId\\)`,
),
),
true,
`${table} should be loaded from the enrichment evidence run.`,
);
}
assert.equal(
hasPattern(
evidenceSource,
/const\s+screenshots\s*=\s*\[\s*\.\.\.auditCaptureScreenshotsByRun,\s*\.\.\.enrichmentScreenshotsByRun\s*\]/,
),
true,
"Evidence query should include audit-run ScreenshotOne captures and enrichment screenshots.",
);
});
test("truncateWithMarker is byte-capped and marker-safe in persistence", () => {
assert.equal(
hasPattern(auditGenerationSource, /const markerBytes = byteLength\(TRUNCATION_MARKER\);/),

View File

@@ -1,14 +1,11 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
import { LOCAL_AUDIT_SKILL_REGISTRY_SOURCE } from "../lib/ai/local-audit-skill-registry";
import { parseSkillsRegistry, toAuditUsedSkill } from "../lib/skills-registry";
test("parseSkillsRegistry parses v3 yaml metablocks from v2 source", async () => {
const source = await readFile(join(process.cwd(), "v2_elemente", "skills.md"), "utf8");
const parsed = parseSkillsRegistry(source);
test("parseSkillsRegistry parses v3 yaml metablocks from the MVP registry source", () => {
const parsed = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE);
assert.equal(parsed.length, 9);
const visualDesign = parsed.find((entry) => entry.id === "visual-design");
@@ -28,9 +25,8 @@ test("parseSkillsRegistry parses v3 yaml metablocks from v2 source", async () =>
assert.match(instructions, /Beurteile den ersten visuellen Eindruck/);
});
test("toAuditUsedSkill exposes stable ids for v3 registry entries", async () => {
const source = await readFile(join(process.cwd(), "v2_elemente", "skills.md"), "utf8");
const parsed = parseSkillsRegistry(source);
test("toAuditUsedSkill exposes stable ids for v3 registry entries", () => {
const parsed = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE);
const skill = parsed.find((entry) => entry.id === "contact-conversion");
assert.ok(skill);
@@ -40,9 +36,8 @@ test("toAuditUsedSkill exposes stable ids for v3 registry entries", async () =>
});
});
test("parseSkillsRegistry does not infer categories for v3 entries without explicit metadata", async () => {
const source = await readFile(join(process.cwd(), "v2_elemente", "skills.md"), "utf8");
const parsed = parseSkillsRegistry(source);
test("parseSkillsRegistry does not infer categories for v3 entries without explicit metadata", () => {
const parsed = parseSkillsRegistry(LOCAL_AUDIT_SKILL_REGISTRY_SOURCE);
const skill = parsed.find((entry) => entry.id === "performance-experience");
assert.ok(skill);

View File

@@ -225,8 +225,8 @@ test("audits.getDetail returns audit + lead context with null-safe lead lookup",
);
hasPattern(
getDetailSource,
/return\s*{\s*audit,\s*lead\s*}/,
"getDetail should return { audit, lead }.",
/return\s*{[\s\S]*audit,[\s\S]*lead,[\s\S]*sourceSummaries:[\s\S]*}/,
"getDetail should return audit, lead, and sourceSummaries.",
);
hasPattern(
sourceFile.getFullText(),
@@ -234,3 +234,48 @@ test("audits.getDetail returns audit + lead context with null-safe lead lookup",
"audits.ts should export a getDetail query.",
);
});
test("audits.getDetail joins compact checked-page evidence from latest successful enrichment", () => {
const getDetailSource = extractExportSource("getDetail");
hasPattern(
getDetailSource,
/query\("agentRuns"\)[\s\S]*withIndex\("by_type_and_status_and_leadId"[\s\S]*eq\("type",\s*"website_enrichment"\)[\s\S]*eq\("status",\s*"succeeded"\)[\s\S]*eq\("leadId",\s*audit\.leadId\)[\s\S]*order\("desc"\)[\s\S]*take\(1\)/,
"getDetail should locate the latest successful website_enrichment run for the audit lead.",
);
for (const table of [
"websiteCrawlPages",
"websiteTechnicalChecks",
"websiteCrawlScreenshots",
]) {
hasPattern(
getDetailSource,
new RegExp(
`query\\("${table}"\\)[\\s\\S]*withIndex\\("by_runId"[\\s\\S]*eq\\("runId",\\s*enrichmentRunId\\)[\\s\\S]*take\\(DETAIL_EVIDENCE_LIMIT\\)`,
),
`${table} should be loaded from the bounded enrichment run evidence window.`,
);
}
hasPattern(
getDetailSource,
/audit\.checkedPages\.map\(/,
"getDetail should preserve audit.checkedPages as the canonical display order.",
);
hasPattern(
getDetailSource,
/fallbackCheckedPageEvidence/,
"getDetail should return checked-page fallback rows when enrichment evidence is missing.",
);
hasPattern(
getDetailSource,
/ctx\.storage\.getUrl\(screenshot\.storageId\)/,
"getDetail should resolve screenshot storage ids to display URLs.",
);
hasPattern(
getDetailSource,
/sourceSummaries:\s*{\s*checkedPages/,
"getDetail should expose checked page summaries under sourceSummaries.checkedPages.",
);
});

View File

@@ -141,6 +141,53 @@ test("audit detail component uses getDetail query and renders skills overview se
);
});
test("audit detail component renders compact checked-page evidence", async () => {
const detailSource = await source("components/audits/audit-detail.tsx");
assert.match(
detailSource,
/sourceSummaries/,
"AuditDetail should read sourceSummaries from getDetail.",
);
assert.match(
detailSource,
/checkedPageEvidence/,
"AuditDetail should derive checked page evidence from sourceSummaries.checkedPages.",
);
assert.match(
detailSource,
/Geprüfte Seiten/,
"AuditDetail should render a checked-pages evidence card.",
);
assert.match(
detailSource,
/checkedPageEvidence\.map/,
"AuditDetail should render one compact row per checked page.",
);
for (const label of [
"Meta",
"Kontaktformular",
"CTA",
"Interne Links",
]) {
assert.match(
detailSource,
new RegExp(label),
`AuditDetail should expose ${label} evidence for each page.`,
);
}
assert.match(
detailSource,
/page\.screenshots\.map/,
"AuditDetail should render optional screenshot thumbnails when present.",
);
assert.match(
detailSource,
/<img[\s\S]*src=\{screenshot\.url\}/,
"AuditDetail should render screenshot URLs from the detail query.",
);
});
test("audits detail route passes id to AuditDetail via Promise params", async () => {
const pageSource = await source("app/dashboard/audits/[id]/page.tsx");

View File

@@ -33,6 +33,9 @@ test("settings page surfaces integration status instead of a placeholder", () =>
assert.doesNotMatch(helperSource, /requiredEnv: \["TASK8_BROWSER_ASSET_URL"\]/);
assert.match(helperSource, /requiredEnv: \["SCREENSHOTONE_API_KEY"\]/);
assert.match(helperSource, /requiredEnv: \[\]/);
assert.match(componentSource, /Next\.js-Runtime/);
assert.match(componentSource, /Convex-Action-Env/);
assert.match(helperSource, /Convex-Run-Events/);
});
test("verification notes cover critical MVP flows", () => {