191 lines
5.3 KiB
Go
191 lines
5.3 KiB
Go
package eval
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func setupReportDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schema := `
|
|
CREATE TABLE jobs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
url TEXT UNIQUE NOT NULL,
|
|
company TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
source TEXT,
|
|
discovered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE applications (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
job_id INTEGER NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'Evaluated',
|
|
score REAL,
|
|
archetype TEXT,
|
|
legitimacy TEXT,
|
|
evaluated_at TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE TABLE reports (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
application_id INTEGER NOT NULL,
|
|
report_md TEXT,
|
|
seq INTEGER,
|
|
slug TEXT,
|
|
legitimacy TEXT,
|
|
score REAL,
|
|
block_a TEXT, block_b TEXT, block_c TEXT, block_d TEXT,
|
|
block_e TEXT, block_f TEXT, block_g TEXT,
|
|
file_path TEXT,
|
|
stories TEXT,
|
|
generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
`
|
|
if _, err := db.Exec(schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestSlugify(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Acme Corp": "acme-corp",
|
|
"Dragos, Inc.": "dragos-inc",
|
|
" Red/Canary ": "redcanary",
|
|
"../../etc/passwd": "etcpasswd",
|
|
"": "unknown",
|
|
"!@#$%^&*()": "unknown",
|
|
"Foo ___ Bar": "foo-bar",
|
|
strings.Repeat("x", 80): strings.Repeat("x", 64),
|
|
}
|
|
for in, want := range cases {
|
|
if got := Slugify(in); got != want {
|
|
t.Errorf("Slugify(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReportWriter_SaveEndToEnd(t *testing.T) {
|
|
db := setupReportDB(t)
|
|
defer db.Close()
|
|
tmp := t.TempDir()
|
|
w := &ReportWriter{DB: db, ReportsDir: tmp}
|
|
|
|
job := JobContext{
|
|
URL: "https://example.com/jobs/42",
|
|
Company: "Acme Corp",
|
|
Title: "Staff LLMOps",
|
|
Source: "greenhouse",
|
|
JDText: "Job description body.",
|
|
}
|
|
eval := Evaluation{
|
|
Archetype: "LLMOps",
|
|
Score: 4.2,
|
|
Legitimacy: TierHigh,
|
|
BlockA: "## A) Role Summary\n\nStaff LLMOps role.",
|
|
BlockB: "## B) CV Match\n\nStrong match.\n\n**Weighted score:** 4.2/5",
|
|
BlockG: "## G) Posting Legitimacy\n\n**Assessment:** High Confidence",
|
|
Raw: "full raw",
|
|
}
|
|
|
|
saved, err := w.Save(context.Background(), job, eval)
|
|
if err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
if saved.Seq != 1 {
|
|
t.Errorf("Seq = %d, want 1", saved.Seq)
|
|
}
|
|
if saved.ApplicationID == 0 || saved.ReportID == 0 {
|
|
t.Errorf("missing ids: app=%d report=%d", saved.ApplicationID, saved.ReportID)
|
|
}
|
|
if filepath.Dir(saved.FilePath) != tmp {
|
|
t.Errorf("file path outside ReportsDir: %q", saved.FilePath)
|
|
}
|
|
if _, err := os.Stat(saved.FilePath); err != nil {
|
|
t.Fatalf("report file missing: %v", err)
|
|
}
|
|
|
|
// Contents exercise RenderMarkdown: Block G appears before Block A.
|
|
body, err := os.ReadFile(saved.FilePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := string(body)
|
|
if !strings.Contains(s, "**Legitimacy:** high") {
|
|
t.Errorf("missing legitimacy header in report:\n%s", s)
|
|
}
|
|
gIdx := strings.Index(s, "## G)")
|
|
aIdx := strings.Index(s, "## A)")
|
|
if gIdx == -1 || aIdx == -1 || gIdx > aIdx {
|
|
t.Errorf("Block G should appear before Block A; gIdx=%d aIdx=%d", gIdx, aIdx)
|
|
}
|
|
|
|
// Save second eval for the same URL: seq should become 2, application
|
|
// should be updated (not duplicated).
|
|
eval.Score = 4.5
|
|
saved2, err := w.Save(context.Background(), job, eval)
|
|
if err != nil {
|
|
t.Fatalf("Save#2: %v", err)
|
|
}
|
|
if saved2.Seq != 2 {
|
|
t.Errorf("Seq#2 = %d, want 2", saved2.Seq)
|
|
}
|
|
if saved2.ApplicationID != saved.ApplicationID {
|
|
t.Errorf("application_id drifted: %d → %d", saved.ApplicationID, saved2.ApplicationID)
|
|
}
|
|
var appCount int
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM applications`).Scan(&appCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if appCount != 1 {
|
|
t.Errorf("applications count = %d, want 1", appCount)
|
|
}
|
|
var reportCount int
|
|
if err := db.QueryRow(`SELECT COUNT(*) FROM reports`).Scan(&reportCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reportCount != 2 {
|
|
t.Errorf("reports count = %d, want 2", reportCount)
|
|
}
|
|
}
|
|
|
|
func TestReportWriter_StatusPreservedWhenReEvaluated(t *testing.T) {
|
|
db := setupReportDB(t)
|
|
defer db.Close()
|
|
w := &ReportWriter{DB: db, ReportsDir: t.TempDir()}
|
|
|
|
job := JobContext{URL: "https://example.com/j/1", Company: "Beta", Title: "SWE", Source: "ashby"}
|
|
eval := Evaluation{Score: 3.0, Legitimacy: TierCaution, BlockA: "## A) x", BlockG: "## G) y"}
|
|
|
|
if _, err := w.Save(context.Background(), job, eval); err != nil {
|
|
t.Fatalf("first save: %v", err)
|
|
}
|
|
// Simulate user advancing the pipeline.
|
|
if _, err := db.Exec(`UPDATE applications SET status = 'Applied' WHERE job_id = (SELECT id FROM jobs WHERE url = ?)`, job.URL); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Re-run eval — must NOT clobber 'Applied' back to 'Evaluated'.
|
|
eval.Score = 3.5
|
|
if _, err := w.Save(context.Background(), job, eval); err != nil {
|
|
t.Fatalf("second save: %v", err)
|
|
}
|
|
var status string
|
|
if err := db.QueryRow(`SELECT status FROM applications WHERE job_id = (SELECT id FROM jobs WHERE url = ?)`, job.URL).Scan(&status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status != "Applied" {
|
|
t.Errorf("status = %q, want Applied (re-eval should not regress)", status)
|
|
}
|
|
}
|