test: execute AI evaluation report normalization

dev-QQq
chenyuan 1 month ago
parent bed1cc47cd
commit 33b113e9cb

@ -61,6 +61,9 @@
<script setup> <script setup>
import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation"; import { getAiTrainingEvaluation, requestAiTrainingAssessment, requestAiTrainingHelp } from "@/api/aiTrainingEvaluation";
import reportNormalizer from "./trainingAiEvaluationReport.cjs";
const { normalizeEvaluationReports, normalizeOperation } = reportNormalizer;
const props = defineProps({ const props = defineProps({
taskKey: { type: String, default: "" }, taskKey: { type: String, default: "" },
@ -74,15 +77,15 @@ const assessmentLoading = ref(false);
const helpError = ref(""); const helpError = ref("");
const assessmentError = ref(""); const assessmentError = ref("");
const help = computed(() => normalizeOperation(evaluation.value.help || evaluation.value.helpEvaluation, evaluation.value, "help")); const normalizedReports = computed(() => normalizeEvaluationReports(evaluation.value));
const assessment = computed(() => normalizeOperation(evaluation.value.assessment || evaluation.value.assessmentEvaluation, evaluation.value, "assessment")); const help = computed(() => normalizedReports.value.help);
const assessment = computed(() => normalizedReports.value.assessment);
const helpStatus = computed(() => help.value.status); const helpStatus = computed(() => help.value.status);
const assessmentStatus = computed(() => assessment.value.status); const assessmentStatus = computed(() => assessment.value.status);
const helpReport = computed(() => formatReport(help.value.report || help.value.content || help.value.suggestion)); const helpReport = computed(() => normalizedReports.value.helpReport);
const assessmentReport = computed(() => formatReport(assessment.value.report || assessment.value.content || assessment.value.feedback)); const assessmentReport = computed(() => normalizedReports.value.assessmentReport);
const assessmentReportData = computed(() => safeParseJson(evaluation.value.assessmentReportJson)); const score = computed(() => normalizedReports.value.score);
const score = computed(() => evaluation.value.assessmentScore ?? assessment.value.score ?? assessmentReportData.value?.score ?? evaluation.value.score ?? null); const scoreCriteria = computed(() => normalizedReports.value.scoreCriteria);
const scoreCriteria = computed(() => normalizeCriteria(assessmentReportData.value?.criteria || assessment.value.scoreCriteria || evaluation.value.scoreCriteria));
const isHelpDisabled = computed(() => !props.hasAnyAnswer || helpLoading.value || isTerminal(helpStatus.value)); const isHelpDisabled = computed(() => !props.hasAnyAnswer || helpLoading.value || isTerminal(helpStatus.value));
const isAssessmentDisabled = computed(() => !props.hasAnyAnswer || assessmentLoading.value || isTerminal(assessmentStatus.value)); const isAssessmentDisabled = computed(() => !props.hasAnyAnswer || assessmentLoading.value || isTerminal(assessmentStatus.value));
const helpActionLabel = computed(() => actionLabel(helpStatus.value, "生成助学建议", helpLoading.value)); const helpActionLabel = computed(() => actionLabel(helpStatus.value, "生成助学建议", helpLoading.value));
@ -173,51 +176,6 @@ function setLocalStatus(type, status) {
}; };
} }
function normalizeOperation(operation, root, type) {
const value = operation && typeof operation === "object" ? operation : {};
const prefix = type === "help" ? "help" : "assessment";
const flatReportKey = type === "help" ? "helpReportJson" : "assessmentReportJson";
return {
...value,
status: String(value.status || root?.[`${prefix}Status`] || "PENDING").toUpperCase(),
report: value.report || root?.[flatReportKey] || root?.[`${prefix}Report`] || "",
scoreCriteria: value.scoreCriteria || root?.scoreCriteria || [],
};
}
function normalizeCriteria(value) {
if (!Array.isArray(value)) return [];
return value.map((item) => {
if (typeof item === "string") return { name: item, score: "", description: "" };
return {
name: String(item?.name || item?.criterion || item?.title || "评分项"),
score: item?.score ?? item?.weight ?? "",
description: String(item?.description || item?.detail || ""),
};
});
}
function formatReport(value) {
if (value == null) return "";
const parsed = typeof value === "string" ? safeParseJson(value) : value;
if (parsed == null) return typeof value === "string" ? value : "";
try {
return JSON.stringify(parsed, null, 2);
} catch (error) {
return String(value);
}
}
function safeParseJson(value) {
if (typeof value !== "string" || !value.trim()) return null;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch (error) {
return null;
}
}
function isTerminal(status) { function isTerminal(status) {
return ["PROCESSING", "SUCCEEDED"].includes(status); return ["PROCESSING", "SUCCEEDED"].includes(status);
} }

@ -0,0 +1,68 @@
function safeParseJson(value) {
if (typeof value !== "string" || !value.trim()) return null;
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch (error) {
return null;
}
}
function formatReport(value) {
if (value == null) return "";
const parsed = typeof value === "string" ? safeParseJson(value) : value;
if (parsed == null) return typeof value === "string" ? value : "";
try {
return JSON.stringify(parsed, null, 2);
} catch (error) {
return String(value);
}
}
function normalizeCriteria(value) {
if (!Array.isArray(value)) return [];
return value.map((item) => {
if (typeof item === "string") return { name: item, score: "", description: "" };
return {
name: String(item?.name || item?.criterion || item?.title || "评分项"),
score: item?.score ?? item?.weight ?? "",
description: String(item?.description || item?.detail || ""),
};
});
}
function normalizeOperation(operation, root, type) {
const value = operation && typeof operation === "object" ? operation : {};
const prefix = type === "help" ? "help" : "assessment";
const flatReportKey = type === "help" ? "helpReportJson" : "assessmentReportJson";
return {
...value,
status: String(value.status || root?.[`${prefix}Status`] || "PENDING").toUpperCase(),
report: value.report || root?.[flatReportKey] || root?.[`${prefix}Report`] || "",
scoreCriteria: value.scoreCriteria || root?.scoreCriteria || [],
};
}
function normalizeEvaluationReports(evaluation) {
const root = evaluation && typeof evaluation === "object" ? evaluation : {};
const help = normalizeOperation(root.help || root.helpEvaluation, root, "help");
const assessment = normalizeOperation(root.assessment || root.assessmentEvaluation, root, "assessment");
const assessmentReportData = safeParseJson(root.assessmentReportJson);
return {
help,
assessment,
helpReport: formatReport(help.report || help.content || help.suggestion),
assessmentReport: formatReport(assessment.report || assessment.content || assessment.feedback),
score: root.assessmentScore ?? assessment.score ?? assessmentReportData?.score ?? root.score ?? null,
scoreCriteria: normalizeCriteria(assessmentReportData?.criteria || assessment.scoreCriteria || root.scoreCriteria),
};
}
module.exports = {
formatReport,
normalizeCriteria,
normalizeEvaluationReports,
normalizeOperation,
safeParseJson,
};

@ -6,6 +6,7 @@ const root = path.resolve(__dirname, "..");
const api = fs.readFileSync(path.join(root, "src/api/aiTrainingEvaluation.js"), "utf8"); const api = fs.readFileSync(path.join(root, "src/api/aiTrainingEvaluation.js"), "utf8");
const component = fs.readFileSync(path.join(root, "src/views/components/TrainingAiEvaluation.vue"), "utf8"); const component = fs.readFileSync(path.join(root, "src/views/components/TrainingAiEvaluation.vue"), "utf8");
const page = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8"); const page = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8");
const { normalizeEvaluationReports } = require(path.join(root, "src/views/components/trainingAiEvaluationReport.cjs"));
assert(/`\/api\/student\/training-tasks\/\$\{taskKey\}\/ai-evaluation`/.test(api), "evaluation API should use the student task endpoint"); assert(/`\/api\/student\/training-tasks\/\$\{taskKey\}\/ai-evaluation`/.test(api), "evaluation API should use the student task endpoint");
assert(/\/help/.test(api) && /\/assessment/.test(api), "evaluation API should expose help and assessment operations"); assert(/\/help/.test(api) && /\/assessment/.test(api), "evaluation API should expose help and assessment operations");
@ -20,8 +21,38 @@ assert(/!props\.hasAnyAnswer/.test(component), "AI actions should disable when n
assert(/proxy\?\.\$modal\?\.confirm/.test(component), "assessment should request confirmation through the modal proxy"); assert(/proxy\?\.\$modal\?\.confirm/.test(component), "assessment should request confirmation through the modal proxy");
assert(!/v-html/.test(component), "AI reports must be rendered as interpolated text, never v-html"); assert(!/v-html/.test(component), "AI reports must be rendered as interpolated text, never v-html");
assert(/scoreCriteria/.test(component), "assessment should display score criteria"); assert(/scoreCriteria/.test(component), "assessment should display score criteria");
assert(/helpReportJson/.test(component) && /assessmentReportJson/.test(component), "flat backend report JSON fields should be mapped for display"); assert(/normalizeEvaluationReports/.test(component), "the component should use the executable report normalizer");
assert(/assessmentScore/.test(component), "flat backend assessmentScore should be mapped to the displayed score");
assert(/safeParseJson/.test(component), "flat report JSON should be parsed defensively"); const normalizedFlatPayload = normalizeEvaluationReports({
assert(/safeParseJson\([^)]*assessmentReportJson[^)]*\)[\s\S]*criteria|criteria[\s\S]*safeParseJson\([^)]*assessmentReportJson[^)]*\)/.test(component), "score criteria should be read from parsed assessmentReportJson"); helpStatus: "succeeded",
helpReportJson: JSON.stringify({ summary: "Add supporting data", suggestions: ["Cite the source"] }),
assessmentStatus: "succeeded",
assessmentScore: 92,
assessmentReportJson: JSON.stringify({
summary: "Clear analysis",
criteria: [
{ criterion: "Evidence", weight: 40, detail: "Uses relevant data" },
"Structure",
],
}),
});
assert.strictEqual(normalizedFlatPayload.help.status, "SUCCEEDED", "flat help status should be normalized");
assert.match(normalizedFlatPayload.helpReport, /Add supporting data/, "flat help JSON should be formatted for text display");
assert.strictEqual(normalizedFlatPayload.assessment.status, "SUCCEEDED", "flat assessment status should be normalized");
assert.strictEqual(normalizedFlatPayload.score, 92, "assessmentScore should take precedence for the displayed score");
assert.deepStrictEqual(normalizedFlatPayload.scoreCriteria, [
{ name: "Evidence", score: 40, description: "Uses relevant data" },
{ name: "Structure", score: "", description: "" },
], "assessment criteria should be mapped from the flat assessment JSON");
const malformedPayload = normalizeEvaluationReports({
helpReportJson: "{not valid JSON",
assessmentReportJson: "{also not valid JSON",
});
assert.strictEqual(malformedPayload.helpReport, "{not valid JSON", "malformed help JSON should remain safe text");
assert.strictEqual(malformedPayload.assessmentReport, "{also not valid JSON", "malformed assessment JSON should remain safe text");
assert.strictEqual(malformedPayload.score, null, "malformed assessment JSON should not produce a score");
assert.deepStrictEqual(malformedPayload.scoreCriteria, [], "malformed assessment JSON should not produce criteria");
assert(/retry/.test(component) || /重试/.test(component), "failed AI operations should expose a retry action"); assert(/retry/.test(component) || /重试/.test(component), "failed AI operations should expose a retry action");

Loading…
Cancel
Save