feat(ui): migrate Career import review and version history

This commit is contained in:
cesnimda
2026-10-12 19:28:25 +02:00
parent ec23b650f7
commit 5d6e32358c
20 changed files with 10454 additions and 124 deletions
@@ -0,0 +1,287 @@
"""Test-only Career import-review seeder; never a product endpoint.
Input JSON on stdin: dataRoot, ownerId, ownerEmail, profile; optional rawText and
normalizedText (string or null). Output: {"runId": <inserted integer>}.
The database must already exist as <temp>/jobtracker-e2e-*/jobtracker.db.
Run --self-check to exercise only this helper's own temporary SQLite fixtures.
"""
from __future__ import annotations
import json
import re
import sqlite3
import subprocess
import sys
import tempfile
from contextlib import closing
from datetime import datetime, timezone
from pathlib import Path
class SeedDenied(ValueError):
"""The requested input is outside this isolated test fixture contract."""
RUN_SCHEMA = {
"Id": ("INTEGER", 1, 1),
"OwnerUserId": ("TEXT", 1, 0),
"ArtifactId": ("INTEGER", 0, 0),
"Trigger": ("TEXT", 1, 0),
"ParserVersion": ("TEXT", 1, 0),
"NormalizerVersion": ("TEXT", 1, 0),
"LlmPromptVersion": ("TEXT", 1, 0),
"Status": ("TEXT", 1, 0),
"RawExtractedText": ("TEXT", 0, 0),
"NormalizedText": ("TEXT", 0, 0),
"StructuredProfileJson": ("TEXT", 0, 0),
"ErrorMessage": ("TEXT", 0, 0),
"StartedAtUtc": ("TEXT", 1, 0),
"CompletedAtUtc": ("TEXT", 0, 0),
"AppliedAtUtc": ("TEXT", 0, 0),
}
def seed_review(payload: object) -> int:
if not isinstance(payload, dict):
raise SeedDenied("Input must be a JSON object")
allowed = {"dataRoot", "ownerId", "ownerEmail", "profile", "rawText", "normalizedText"}
if set(payload) - allowed:
raise SeedDenied("Unsupported input keys")
for key in ("dataRoot", "ownerId", "ownerEmail"):
if not isinstance(payload.get(key), str) or not payload[key].strip():
raise SeedDenied(f"{key} must be a nonempty string")
if not isinstance(payload.get("profile"), dict):
raise SeedDenied("profile must be a JSON object")
for key in ("rawText", "normalizedText"):
if payload.get(key) is not None and not isinstance(payload[key], str):
raise SeedDenied(f"{key} must be text or null")
try:
structured_json = json.dumps(payload["profile"], ensure_ascii=False, separators=(",", ":"), allow_nan=False)
except (TypeError, ValueError) as error:
raise SeedDenied("profile must contain finite JSON values") from error
requested_root = Path(payload["dataRoot"])
if not requested_root.is_absolute():
raise SeedDenied("dataRoot must be absolute")
try:
root = requested_root.resolve(strict=True)
temp_root = Path(tempfile.gettempdir()).resolve(strict=True)
except OSError as error:
raise SeedDenied("dataRoot must already exist") from error
if not root.is_dir() or root.parent != temp_root or not re.fullmatch(r"jobtracker-e2e-[A-Za-z0-9][A-Za-z0-9_-]*", root.name):
raise SeedDenied("dataRoot must be a jobtracker-e2e-* direct child of the system temp directory")
database = root / "jobtracker.db"
if not database.is_file() or database.resolve(strict=True) != database or database.stat().st_nlink != 1:
raise SeedDenied("The existing direct jobtracker.db file is required")
prohibited_actions = {
sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE,
sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DETACH, sqlite3.SQLITE_ALTER_TABLE,
sqlite3.SQLITE_CREATE_TABLE, sqlite3.SQLITE_CREATE_INDEX, sqlite3.SQLITE_CREATE_TRIGGER,
sqlite3.SQLITE_CREATE_VIEW, sqlite3.SQLITE_CREATE_TEMP_TABLE, sqlite3.SQLITE_CREATE_TEMP_INDEX,
sqlite3.SQLITE_CREATE_TEMP_TRIGGER, sqlite3.SQLITE_CREATE_TEMP_VIEW,
sqlite3.SQLITE_DROP_TABLE, sqlite3.SQLITE_DROP_INDEX, sqlite3.SQLITE_DROP_TRIGGER,
sqlite3.SQLITE_DROP_VIEW, sqlite3.SQLITE_DROP_TEMP_TABLE, sqlite3.SQLITE_DROP_TEMP_INDEX,
sqlite3.SQLITE_DROP_TEMP_TRIGGER, sqlite3.SQLITE_DROP_TEMP_VIEW,
}
def authorize(action: int, target: str | None, _column: str | None,
_database: str | None, _trigger: str | None) -> int:
if action in prohibited_actions:
return sqlite3.SQLITE_DENY
if action == sqlite3.SQLITE_INSERT and target != "CvExtractionRuns":
return sqlite3.SQLITE_DENY
if action == sqlite3.SQLITE_PRAGMA and target != "table_info":
return sqlite3.SQLITE_DENY
return sqlite3.SQLITE_OK
# mode=rw refuses to create a database. Closing is explicit for Windows locks.
with closing(sqlite3.connect(database.as_uri() + "?mode=rw", uri=True, timeout=5)) as connection, connection:
connection.execute("PRAGMA foreign_keys=ON")
connection.set_authorizer(authorize)
connection.execute("BEGIN IMMEDIATE")
actual = {row[1]: (row[2].upper(), row[3], row[5])
for row in connection.execute('PRAGMA table_info("CvExtractionRuns")')}
if actual != RUN_SCHEMA:
raise SeedDenied("CvExtractionRuns schema does not match the existing fixture contract")
users = {row[1]: row[2].upper() for row in connection.execute('PRAGMA table_info("AspNetUsers")')}
if users.get("Id") != "TEXT" or users.get("Email") != "TEXT":
raise SeedDenied("Existing AspNetUsers Id/Email schema is required")
owner = connection.execute('SELECT "Id", "Email" FROM "AspNetUsers" WHERE "Id" = ?', (payload["ownerId"],)).fetchone()
if owner != (payload["ownerId"], payload["ownerEmail"]):
raise SeedDenied("Owner ID and email must exactly match an existing real fixture account")
if connection.execute("SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='CvExtractionRuns' LIMIT 1").fetchone():
raise SeedDenied("CvExtractionRuns triggers are outside this insert-only fixture contract")
timestamp = datetime.now(timezone.utc).isoformat(timespec="microseconds").replace("T", " ")
cursor = connection.execute(
'INSERT INTO "CvExtractionRuns" ("OwnerUserId", "ArtifactId", "Trigger", "ParserVersion", "NormalizerVersion", "LlmPromptVersion", "Status", "RawExtractedText", "NormalizedText", "StructuredProfileJson", "ErrorMessage", "StartedAtUtc", "CompletedAtUtc", "AppliedAtUtc") VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL)',
(payload["ownerId"], "upload", "native-offline-fixture", "native-offline-fixture",
"native-offline-fixture", "pending_review", payload.get("rawText"),
payload.get("normalizedText"), structured_json, timestamp, timestamp),
)
return int(cursor.lastrowid)
def self_check() -> dict:
"""No repo/live database is opened; every database belongs to this check."""
passed: list[str] = []
profile = {
"version": "1", "contact": {},
"languages": [{"name": "French"}, {"name": "Norwegian", "level": "B1"}],
}
owner_id, owner_email = "self-check-owner", "self-check@example.test"
def fixture(root: Path, *, schema: str = "correct", owner: str = owner_id,
email: str = owner_email) -> Path:
database = root / "jobtracker.db"
columns = []
for name, (kind, required, primary) in RUN_SCHEMA.items():
if schema == "missing_column" and name == "StructuredProfileJson":
continue
if schema == "wrong_type" and name == "Status":
kind = "INTEGER"
if schema == "wrong_nullability" and name == "Status":
required = 0
columns.append(f'"{name}" {kind}' + (" NOT NULL" if required else "")
+ (" PRIMARY KEY AUTOINCREMENT" if primary else ""))
if schema == "extra_column":
columns.append('"UnexpectedColumn" TEXT')
with closing(sqlite3.connect(database)) as connection, connection:
connection.execute('CREATE TABLE "CvExtractionRuns" (' + ",".join(columns) + ")")
if schema != "missing_users":
connection.execute('CREATE TABLE "AspNetUsers" ("Id" TEXT PRIMARY KEY, "Email" TEXT, "ProfileCvText" TEXT, "ProfileCvStructureJson" TEXT)')
connection.execute('INSERT INTO "AspNetUsers" VALUES (?, ?, ?, ?)',
(owner, email, "Canonical raw — æøå", '{"summary":["Canonical source"]}'))
connection.execute('INSERT INTO "AspNetUsers" VALUES (?, ?, ?, ?)',
("other-owner", "other@example.test", "Other raw", '{"summary":["Other source"]}'))
connection.execute('CREATE TABLE "CareerProfiles" ("Id" INTEGER PRIMARY KEY, "OwnerUserId" TEXT, "Version" INTEGER, "Summary" TEXT)')
connection.execute('INSERT INTO "CareerProfiles" VALUES (1, ?, 7, ?)', (owner, "Current canonical — æøå"))
connection.execute('CREATE TABLE "CareerProfileVersions" ("Id" INTEGER PRIMARY KEY, "CareerProfileId" INTEGER, "Version" INTEGER, "ProfileJson" TEXT)')
connection.execute('INSERT INTO "CareerProfileVersions" VALUES (1, 1, 7, ?)', ('{"summary":["Historical source"]}',))
connection.execute('CREATE TABLE "UserOperations" ("Id" TEXT PRIMARY KEY, "OwnerUserId" TEXT, "Status" TEXT)')
connection.execute('INSERT INTO "UserOperations" VALUES (?, ?, ?)', ("existing-operation", owner, "succeeded"))
return database
def payload_for(root: Path, **changes: object) -> dict:
return {"dataRoot": str(root), "ownerId": owner_id, "ownerEmail": owner_email,
"profile": profile, "rawText": "Synthetic raw — æøå",
"normalizedText": "French\nNorwegian B1", **changes}
def snapshot(database: Path) -> dict:
with closing(sqlite3.connect(database)) as connection, connection:
rows = {table: connection.execute(f'SELECT * FROM "{table}" ORDER BY rowid').fetchall()
for table in ("AspNetUsers", "CareerProfiles", "CareerProfileVersions", "UserOperations", "CvExtractionRuns")}
rows["__schema__"] = connection.execute("SELECT type, name, tbl_name, sql FROM sqlite_master ORDER BY type, name").fetchall()
return rows
def denied(name: str, payload: object) -> None:
try:
seed_review(payload)
except SeedDenied:
passed.append(name)
else:
raise AssertionError(f"{name}: seeder accepted a forbidden request")
with tempfile.TemporaryDirectory(prefix="unrelated-career-seed-") as directory:
root = Path(directory).resolve()
fixture(root)
denied("wrong_prefix", payload_for(root))
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve() / "jobtracker-e2e-nested"
root.mkdir()
fixture(root)
denied("nested_root", payload_for(root))
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve()
denied("missing_database", payload_for(root))
assert not (root / "jobtracker.db").exists(), "mode=rw must not create a database"
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve()
database = fixture(root)
(root / "own-fixture-alias.db").hardlink_to(database)
before = snapshot(database)
denied("linked_database_denied", payload_for(root))
assert snapshot(database) == before
for schema in ("missing_column", "extra_column", "wrong_type", "wrong_nullability", "missing_users"):
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve()
fixture(root, schema=schema)
denied(schema, payload_for(root))
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve()
database = fixture(root)
before = snapshot(database)
denied("missing_owner", payload_for(root, ownerId="absent-owner"))
denied("wrong_owner_email", payload_for(root, ownerEmail="other@example.test"))
denied("owner_email_case_is_exact", payload_for(root, ownerEmail=owner_email.upper()))
denied("invalid_profile", payload_for(root, profile=[]))
denied("non_finite_profile_json", payload_for(root, profile={"confidence": float("nan")}))
denied("unknown_payload_key", payload_for(root, trigger="parse"))
denied("invalid_raw_text", payload_for(root, rawText={"not": "text"}))
assert snapshot(database) == before, "Denied requests changed existing data"
passed.append("denied_requests_preserve_all_rows")
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve()
database = fixture(root)
with closing(sqlite3.connect(database)) as connection, connection:
connection.execute('CREATE TRIGGER "unexpected_review_write" AFTER INSERT ON "CvExtractionRuns" BEGIN UPDATE "AspNetUsers" SET "ProfileCvText" = \'mutated\'; END')
before = snapshot(database)
denied("trigger_cannot_mutate_users", payload_for(root))
assert snapshot(database) == before, "Forbidden trigger changed existing data"
with tempfile.TemporaryDirectory(prefix="jobtracker-e2e-seed-selfcheck-") as directory:
root = Path(directory).resolve()
quoted_id, quoted_email = "owner' OR 1=1 --", "quote'@example.test"
database = fixture(root, owner=quoted_id, email=quoted_email)
before = snapshot(database)
submitted = {**profile, "contact": {"fullName": "Robert'); DROP TABLE AspNetUsers; -- æøå"}}
run_id = seed_review(payload_for(root, ownerId=quoted_id, ownerEmail=quoted_email, profile=submitted))
after = snapshot(database)
assert isinstance(run_id, int) and run_id > 0
assert all(after[table] == rows for table, rows in before.items() if table != "CvExtractionRuns"), "Seeder changed existing source/user/operation data"
assert len(after["CvExtractionRuns"]) == len(before["CvExtractionRuns"]) + 1
with closing(sqlite3.connect(database)) as connection, connection:
connection.row_factory = sqlite3.Row
row = dict(connection.execute('SELECT * FROM "CvExtractionRuns" WHERE "Id" = ?', (run_id,)).fetchone())
assert row["OwnerUserId"] == quoted_id and row["Status"] == "pending_review"
assert row["Trigger"] == "upload" and row["ArtifactId"] is None
assert row["ErrorMessage"] is None and row["AppliedAtUtc"] is None
assert row["ParserVersion"] == row["NormalizerVersion"] == row["LlmPromptVersion"] == "native-offline-fixture"
assert row["RawExtractedText"] == "Synthetic raw — æøå" and row["NormalizedText"] == "French\nNorwegian B1"
assert json.loads(row["StructuredProfileJson"]) == submitted
assert datetime.fromisoformat(row["StartedAtUtc"]).utcoffset().total_seconds() == 0
assert row["CompletedAtUtc"] == row["StartedAtUtc"]
passed.extend(("parameterized_exact_insert", "null_artifact_and_no_operation", "existing_source_user_history_unchanged", "existing_schema_unchanged"))
result = subprocess.run(
[sys.executable, "-X", "utf8", str(Path(__file__).resolve())],
input=json.dumps(payload_for(root, ownerId=quoted_id, ownerEmail=quoted_email, profile=submitted), ensure_ascii=False),
text=True, encoding="utf-8", capture_output=True, check=False,
)
assert result.returncode == 0 and result.stderr == "", result.stderr
returned = json.loads(result.stdout)
assert set(returned) == {"runId"} and returned["runId"] > run_id
final = snapshot(database)
assert final["CvExtractionRuns"][:-1] == after["CvExtractionRuns"], "CLI modified an existing run"
assert len(final["CvExtractionRuns"]) == len(after["CvExtractionRuns"]) + 1
assert all(final[table] == rows for table, rows in after.items() if table != "CvExtractionRuns")
assert json.loads(final["CvExtractionRuns"][-1][10]) == submitted
passed.extend(("stdin_json_returns_only_run_id", "cli_preserves_existing_run_and_source_rows"))
return {"selfCheck": "PASS", "checks": len(passed), "passed": passed, "databaseScope": "own temporary SQLite only"}
def main() -> int:
try:
if sys.argv[1:] == ["--self-check"]:
print(json.dumps(self_check(), ensure_ascii=False))
elif sys.argv[1:]:
raise SeedDenied("Only stdin JSON or --self-check is supported")
else:
print(json.dumps({"runId": seed_review(json.load(sys.stdin))}))
return 0
except (SeedDenied, json.JSONDecodeError, sqlite3.Error) as error:
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())