Files
sm-quizzes-plugin/classes/grading_test.go
Jakub Zych adc02d7e0c fix: withhold grader feedback that repeats the rubric
WR-01: a deterministic server-side guard behind the grader's prompt.
storeGrade drops feedback sharing a run of six words with the rubric
(runs also in the question or model answer do not count) and notes it
for the owner in grading_error.
2026-11-09 22:46:41 +01:00

106 lines
4.1 KiB
Go

package classes_test
import (
"context"
"errors"
"testing"
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
"git.golem15.com/golem15/sm-quizzes-plugin/graders"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
)
type stubGrader struct{ code string }
func (g stubGrader) Code() string { return g.code }
func (g stubGrader) Label() string { return g.code }
func (g stubGrader) Grade(context.Context, graders.Input) (graders.Result, error) {
return graders.Result{}, nil
}
func TestGraderFor(t *testing.T) {
reg := graders.NewRegistry()
if err := reg.Add("acme.x", stubGrader{"x"}); err != nil {
t.Fatal(err)
}
if g, ok := classes.GraderFor(reg, &models.Quiz{Grader: "x"}); !ok || g.Code() != "x" {
t.Errorf("GraderFor(x) = %v, %v", g, ok)
}
for _, code := range []string{graders.Manual, "unknown", ""} {
if _, ok := classes.GraderFor(reg, &models.Quiz{Grader: code}); ok {
t.Errorf("GraderFor(%q) = true, want false", code)
}
}
if _, ok := classes.GraderFor(nil, &models.Quiz{Grader: "x"}); ok {
t.Error("GraderFor(nil registry) = true")
}
if _, ok := classes.GraderFor(reg, nil); ok {
t.Error("GraderFor(nil quiz) = true")
}
}
func TestNextGrading(t *testing.T) {
str := func(s string) *string { return &s }
boom := errors.New("boom")
cases := []struct {
name string
status *string
rowSeq int
jobSeq int
attempts int
err error
want classes.GradingStep
}{
{"nil status", nil, 1, 1, 1, nil, classes.GradingSkip},
{"graded", str(models.GradingGraded), 1, 1, 1, nil, classes.GradingSkip},
{"failed", str(models.GradingFailed), 1, 1, 1, nil, classes.GradingSkip},
{"reviewed", str(models.GradingReviewed), 1, 1, 1, nil, classes.GradingSkip},
{"stale seq", str(models.GradingPending), 2, 1, 1, nil, classes.GradingSkip},
{"stale seq with error", str(models.GradingPending), 2, 1, 3, boom, classes.GradingSkip},
{"store", str(models.GradingPending), 1, 1, 1, nil, classes.GradingStore},
{"retry 1", str(models.GradingPending), 1, 1, 1, boom, classes.GradingRetry},
{"retry 2", str(models.GradingPending), 1, 1, 2, boom, classes.GradingRetry},
{"fail at max", str(models.GradingPending), 1, 1, classes.GradeMaxAttempts, boom, classes.GradingFail},
{"fail past max", str(models.GradingPending), 1, 1, classes.GradeMaxAttempts + 1, boom, classes.GradingFail},
}
for _, tc := range cases {
if got := classes.NextGrading(tc.status, tc.rowSeq, tc.jobSeq, tc.attempts, tc.err); got != tc.want {
t.Errorf("%s: NextGrading = %v, want %v", tc.name, got, tc.want)
}
}
if classes.GradeMaxAttempts != 3 {
t.Errorf("GradeMaxAttempts = %d, want 3", classes.GradeMaxAttempts)
}
}
func TestLeaksRubric(t *testing.T) {
rubric := "Pełne punkty: wymienia trzy przyczyny upadku Republiki Weimarskiej, w tym kryzys gospodarczy z 1929 roku."
question := "Wyjaśnij, dlaczego upadła Republika Weimarska."
model := "Republika upadła przez kryzys gospodarczy z 1929 roku i słabość instytucji."
cases := []struct {
name string
feedback string
want bool
}{
{"own words", "Dobra odpowiedź, ale zabrakło trzeciej przyczyny i dat.", false},
{"verbatim run", "Oczekiwano, że odpowiedź WYMIENIA trzy przyczyny upadku Republiki Weimarskiej.", true},
{"punctuation and case ignored", "pełne punkty - wymienia, trzy; przyczyny upadku republiki!", true},
{"run also in the model answer", "Brakuje wzmianki o tym, że kryzys gospodarczy z 1929 roku i słabość instytucji zaszkodziły.", false},
{"empty feedback", "", false},
}
for _, tc := range cases {
if got := classes.LeaksRubric(tc.feedback, rubric, question, model); got != tc.want {
t.Errorf("%s: LeaksRubric = %v, want %v", tc.name, got, tc.want)
}
}
if classes.LeaksRubric("Za mało szczegółów.", "", question) {
t.Error("an empty rubric leaks")
}
if classes.LeaksRubric("Wymień trzy przyczyny.", "trzy przyczyny", question) {
t.Error("a rubric under the minimum length leaks")
}
if !classes.LeaksRubric("Wymagano: podać daty, nazwiska i skutki. Zabrakło dat.", "Podać daty, nazwiska i skutki.") {
t.Error("a short rubric quoted whole does not leak")
}
}