diff --git a/JobTrackerApi.Tests/CvTemplateRendererTests.cs b/JobTrackerApi.Tests/CvTemplateRendererTests.cs
new file mode 100644
index 0000000..f3554d7
--- /dev/null
+++ b/JobTrackerApi.Tests/CvTemplateRendererTests.cs
@@ -0,0 +1,86 @@
+using JobTrackerApi.Models;
+using JobTrackerApi.Services;
+using Xunit;
+
+namespace JobTrackerApi.Tests;
+
+// Regression safety net for the renderer ahead of the theme-extraction work in
+// career-workspace-implementation-roadmap.md Phase F3. The renderer had zero test coverage;
+// these tests lock in "renders without throwing, is deterministic, and contains the content it
+// was given" for every shipped template so a future refactor (or accidental edit) that changes
+// output is caught immediately.
+public sealed class CvTemplateRendererTests
+{
+ private static readonly string[] AllTemplateIds = { "ats-minimal", "harvard", "auckland", "edinburgh", "monarch", "fjord" };
+
+ private readonly CvTemplateRenderer _renderer = new();
+
+ private static TailoredCvDocument SampleDocument(string templateId) => new()
+ {
+ TemplateId = templateId,
+ Headline = "Senior Backend Engineer",
+ Summary = { "Builds reliable distributed systems." },
+ SelectedSkills = { "C#", ".NET", "SQL" },
+ Experience =
+ {
+ new TailoredCvExperienceItem
+ {
+ Title = "Backend Engineer",
+ Company = "Acme Corp",
+ Start = "2020",
+ End = "Present",
+ IsCurrent = true,
+ Bullets = { "Shipped the payments service." },
+ },
+ },
+ Education =
+ {
+ new TailoredCvEducationItem { Qualification = "BSc Computer Science", Institution = "Example University" },
+ },
+ };
+
+ [Theory]
+ [MemberData(nameof(TemplateIds))]
+ public void Render_produces_html_containing_candidate_and_content(string templateId)
+ {
+ var result = _renderer.Render(SampleDocument(templateId), templateId, "Jamie Rivera", "Backend Engineer", "Acme Corp");
+
+ Assert.Equal(templateId, result.TemplateId);
+ Assert.Contains("Jamie Rivera", result.Html);
+ Assert.Contains("Acme Corp", result.Html);
+ Assert.Contains("Shipped the payments service.", result.Html);
+ Assert.Contains("", result.Html);
+ }
+
+ [Theory]
+ [MemberData(nameof(TemplateIds))]
+ public void Render_is_deterministic_for_identical_input(string templateId)
+ {
+ var a = _renderer.Render(SampleDocument(templateId), templateId, "Jamie Rivera", "Backend Engineer", "Acme Corp");
+ var b = _renderer.Render(SampleDocument(templateId), templateId, "Jamie Rivera", "Backend Engineer", "Acme Corp");
+
+ Assert.Equal(a.Html, b.Html);
+ }
+
+ [Fact]
+ public void Unknown_template_id_falls_back_to_ats_minimal()
+ {
+ var result = _renderer.Render(SampleDocument("unknown"), "not-a-real-template", "Jamie Rivera", "Backend Engineer", null);
+
+ Assert.Equal("ats-minimal", result.TemplateId);
+ }
+
+ [Fact]
+ public void Html_encodes_user_supplied_content_to_prevent_injection()
+ {
+ var document = SampleDocument("ats-minimal");
+ document.Summary[0] = "";
+
+ var result = _renderer.Render(document, "ats-minimal", "Jamie Rivera", "Backend Engineer", null);
+
+ Assert.DoesNotContain("", result.Html);
+ Assert.Contains("<script>", result.Html);
+ }
+
+ public static IEnumerable