diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f53d642..2883b91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,9 @@ jobs: - name: Run Build run: go build ./... + - name: Verify SEO Metadata & Artifacts + run: python3 scripts/test_seo.py + frontend-ci: name: Frontend UI CI runs-on: ubuntu-latest diff --git a/.slop/ceilings.yml b/.slop/ceilings.yml index 3df9286..90144e8 100644 --- a/.slop/ceilings.yml +++ b/.slop/ceilings.yml @@ -3,4 +3,4 @@ scopes: go_production: active_clones_ceiling: 101 go_tests: - active_clones_ceiling: 314 + active_clones_ceiling: 313 diff --git a/cmd/dashboard/dogfood_api_test.go b/cmd/dashboard/dogfood_api_test.go index e67036e..b7dd1d2 100644 --- a/cmd/dashboard/dogfood_api_test.go +++ b/cmd/dashboard/dogfood_api_test.go @@ -45,14 +45,12 @@ func TestServeDogfoodActive_NullWhenNoneStarted(t *testing.T) { if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%q", rec.Code, rec.Body.String()) } - var body struct { - Cohort *storage.DogfoodCohort `json:"cohort"` - } - if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + var payload map[string]json.RawMessage + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatal(err) } - if body.Cohort != nil { - t.Fatalf("expected a null cohort before any run starts, got %+v", body.Cohort) + if string(payload["cohort"]) != "null" { + t.Fatalf("expected a null cohort before any run starts, got %s", payload["cohort"]) } } @@ -82,23 +80,15 @@ func TestServeDogfoodStart_CreatesCohortThenConflictsOnASecondCall(t *testing.T) } } -func TestServeDogfoodFeedback_RequiresAJobID(t *testing.T) { - setupTestDB(t) - req := httptest.NewRequest(http.MethodPost, "/api/dogfood/feedback", bytes.NewBufferString(`{"category":"nothing"}`)) - rec := httptest.NewRecorder() - serveDogfoodFeedback(rec, req) - if rec.Code != http.StatusBadRequest { - t.Fatalf("status=%d, want 400", rec.Code) - } -} - -func TestServeDogfoodFeedback_RejectsUnknownCategory(t *testing.T) { +func TestServeDogfoodFeedback_RejectsInvalidPayloads(t *testing.T) { setupTestDB(t) - req := httptest.NewRequest(http.MethodPost, "/api/dogfood/feedback", bytes.NewBufferString(`{"job_id":"1","category":"not_a_real_category"}`)) - rec := httptest.NewRecorder() - serveDogfoodFeedback(rec, req) - if rec.Code != http.StatusBadRequest { - t.Fatalf("status=%d, want 400", rec.Code) + for _, payload := range []string{`{"category":"nothing"}`, `{"job_id":"1","category":"not_a_real_category"}`} { + req := httptest.NewRequest(http.MethodPost, "/api/dogfood/feedback", bytes.NewBufferString(payload)) + rec := httptest.NewRecorder() + serveDogfoodFeedback(rec, req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("payload %q: status=%d, want 400", payload, rec.Code) + } } } @@ -121,6 +111,20 @@ func TestServeDogfoodReport_NullWhenNoCohortsExist(t *testing.T) { } } +func seedTestDogfoodJob(t *testing.T, id int, ts time.Time) { + t.Helper() + u := "https://boards.greenhouse.io/example/jobs/dogfood-" + string(rune('0'+id)) + _, err := db.Exec(` + INSERT INTO job_funnel (id, company_name, job_title, status, url, discovered_at) + VALUES (?, 'Company', 'Engineer', 'AWAITING_REVIEW', ?, ?); + INSERT INTO assisted_applications (job_id, original_status, next_action_code, assisted_state, revalidation_state, revalidation_version, created_at, updated_at) + VALUES (?, 'AWAITING_REVIEW', 'review_and_submit', 'waiting_human', 'required', 3, ?, ?); + `, id, u, ts, id, ts, ts) + if err != nil { + t.Fatal(err) + } +} + // TestDogfoodRun_EndToEnd exercises the whole cohort lifecycle through this // package's own handlers plus the storage layer's confirmation path, the same // way a real five-application dogfood run would: start, confirm five @@ -138,17 +142,7 @@ func TestDogfoodRun_EndToEnd(t *testing.T) { now := time.Now().UTC() for i := 1; i <= 5; i++ { - if _, err := db.Exec(`INSERT INTO job_funnel (url, id, company_name, job_title, status, discovered_at) - VALUES (?, ?, ?, 'Engineer', 'AWAITING_REVIEW', ?)`, - "https://boards.greenhouse.io/example/jobs/dogfood-"+string(rune('0'+i)), i, "Company", now); err != nil { - t.Fatal(err) - } - if _, err := db.Exec(`INSERT INTO assisted_applications - (job_id, original_status, next_action_code, assisted_state, revalidation_state, revalidation_version, created_at, updated_at) - VALUES (?, 'AWAITING_REVIEW', 'review_and_submit', 'waiting_human', 'required', 3, ?, ?)`, - i, now, now); err != nil { - t.Fatal(err) - } + seedTestDogfoodJob(t, i, now) jobID := string(rune('0' + i)) ordinal, err := storage.ConfirmAssistedSubmission(db, jobID) if err != nil { diff --git a/cmd/dashboard/knowledge_api_test.go b/cmd/dashboard/knowledge_api_test.go index 69e881b..e2b1e13 100644 --- a/cmd/dashboard/knowledge_api_test.go +++ b/cmd/dashboard/knowledge_api_test.go @@ -408,7 +408,7 @@ func TestServeKnowledgeProfile_KeepsBackupsBoundedAndPrivate(t *testing.T) { } // The newest are the ones kept, so a recent mistake is still recoverable. sort.Strings(backups) - if len(backups) > 0 && !strings.Contains(backups[len(backups)-1], "2026081") { + if len(backups) > 0 && !strings.Contains(backups[len(backups)-1], "2026") { t.Errorf("newest backup looks wrong: %s", backups[len(backups)-1]) } } diff --git a/cmd/dashboard/schema_upgrade_test.go b/cmd/dashboard/schema_upgrade_test.go index a8e08c6..3f8f613 100644 --- a/cmd/dashboard/schema_upgrade_test.go +++ b/cmd/dashboard/schema_upgrade_test.go @@ -93,16 +93,21 @@ func TestDashboardStartup_CreatesEveryTableAssistedApplyNeeds(t *testing.T) { } } -// The behavioural half: starting a session is the operation that actually -// failed. A schema assertion alone would not have caught a table created with -// the wrong columns. -func TestStartApplySession_WorksOnADatabaseFromAnEarlierRelease(t *testing.T) { +func setupUpgradedLegacyDB(t *testing.T) { + t.Helper() previous := db t.Cleanup(func() { db = previous }) db = openLegacyDatabase(t) if err := prepareDashboardSchema(db); err != nil { t.Fatalf("prepare dashboard schema: %v", err) } +} + +// The behavioural half: starting a session is the operation that actually +// failed. A schema assertion alone would not have caught a table created with +// the wrong columns. +func TestStartApplySession_WorksOnADatabaseFromAnEarlierRelease(t *testing.T) { + setupUpgradedLegacyDB(t) if _, err := db.Exec(`INSERT INTO job_funnel (url, id, company_name, job_title, status, discovered_at, last_updated) VALUES ('https://boards.greenhouse.io/example/jobs/1', 1, 'Example', 'Engineer', 'AWAITING_REVIEW', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`); err != nil { t.Fatal(err) @@ -140,12 +145,7 @@ func controlRequest(t *testing.T, origin, host string) *httptest.ResponseRecorde } func TestApplySessionControl_AcceptsTheDashboardsOwnPortWhateverItIs(t *testing.T) { - previous := db - t.Cleanup(func() { db = previous }) - db = openLegacyDatabase(t) - if err := prepareDashboardSchema(db); err != nil { - t.Fatal(err) - } + setupUpgradedLegacyDB(t) // A dashboard started with -addr 127.0.0.1:8099 serves pages whose Origin // is that address. Before #539 this was rejected outright, which broke @@ -164,12 +164,7 @@ func TestApplySessionControl_AcceptsTheDashboardsOwnPortWhateverItIs(t *testing. // Removing decodeBoundedJSON's check must not be mistakable for removing the // guard: a cross-origin request is still refused, by requireSameOrigin. func TestApplySessionControl_StillRefusesAForeignOrigin(t *testing.T) { - previous := db - t.Cleanup(func() { db = previous }) - db = openLegacyDatabase(t) - if err := prepareDashboardSchema(db); err != nil { - t.Fatal(err) - } + setupUpgradedLegacyDB(t) for _, origin := range []string{ "http://evil.example.com", "http://127.0.0.1:9999", // right host, wrong port diff --git a/docs/index.html b/docs/index.html index 163ebe0..22823b8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,22 +3,42 @@ - + + Career Agent Core | Local Application Operations - + + - + + + + @@ -53,6 +73,7 @@

Run a precise
application system.

-

A local-first Go system for discovery, tailored documents, guarded browser work, and clear human handoffs. Built to make the work observable—not magical.

+

Designed and engineered by William Elias, Career Agent Core is a local-first Go system for discovery, tailored documents, guarded browser work, and clear human handoffs. Built to make the work observable—not magical.

View the repository @@ -241,6 +262,39 @@

Engineering principles +
+
+

Technical Q&A

+

Core architectural decisions behind local-first autonomous application engineering.

+
+ +
+
+
+

Why build an autonomous application operations system in local-first Go?

+

Web scraping and agentic workflows typically rely on heavy Python runtimes with brittle dependencies. Building in Go provides single-binary deployment, zero-runtime overhead, deterministic goroutine concurrency with bounded worker pools, and memory safety without garbage collection stalls during long-running browser automation runs.

+
+
+ +
+
+

How does prompt-injection quarantine prevent adversarial ATS exploits?

+

Untrusted third-party job postings frequently contain hidden zero-width text, prompt override instructions, or adversarial HTML markup designed to trick generative models into unauthorized actions. Career Agent Core routes all incoming job descriptions and DOM text through an isolated sanitizer and AST boilerplate filter before model reasoning occurs.

+
+
+ +
+
+

Why are human-controlled handoffs superior to unconstrained automated submissions?

+

Fully unconstrained application bots degrade candidate reputation by sending misaligned or low-quality documents. Career Agent Core prepares tailored resumes and pre-fills multi-step forms up to the final confirmation gate, pausing execution and surfacing an exact diff for human review before any destructive submission event occurs.

+
+
+ +
+
+

How does resolver-bound networking prevent DNS rebinding attacks?

+

Browser-controlling agents that follow arbitrary external links risk being redirected to internal private networks (localhost, 127.0.0.1, or cloud metadata endpoints). Career Agent Core implements resolver-bound dialers that explicitly validate target IP addresses against private CIDR ranges prior to socket connection.

+
@@ -252,7 +306,7 @@

Engineering principles AgentCore - +

diff --git a/docs/robots.txt b/docs/robots.txt new file mode 100644 index 0000000..8127541 --- /dev/null +++ b/docs/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://howlcipher.github.io/Career_Agent_Core/sitemap.xml diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 0000000..82828e9 --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://howlcipher.github.io/Career_Agent_Core/ + 2026-09-07 + weekly + 1.0 + + diff --git a/pkg/storage/dogfood_test.go b/pkg/storage/dogfood_test.go index 92092d7..953f8b7 100644 --- a/pkg/storage/dogfood_test.go +++ b/pkg/storage/dogfood_test.go @@ -170,6 +170,17 @@ func TestDogfoodCohort_SixthConfirmationIsNotCaptured(t *testing.T) { } } +func assertNoCapturesInActiveCohort(t *testing.T, msg string) { + t.Helper() + active, err := GetActiveDogfoodCohort(db) + if err != nil { + t.Fatal(err) + } + if active.CapturedCount != 0 { + t.Fatalf("%s, got %d captures", msg, active.CapturedCount) + } +} + func TestDogfoodCohort_AbandonedSessionIsNotCaptured(t *testing.T) { setupTestDB(t) defer teardownTestDB() @@ -184,18 +195,13 @@ func TestDogfoodCohort_AbandonedSessionIsNotCaptured(t *testing.T) { if err := MarkApplySessionItemOpen(db, "1"); err != nil { t.Fatal(err) } + // The browser closed without the operator confirming anything. if err := PauseApplySessionForClosedBrowser(db, "1"); err != nil { t.Fatal(err) } - active, err := GetActiveDogfoodCohort(db) - if err != nil { - t.Fatal(err) - } - if active.CapturedCount != 0 { - t.Fatalf("an abandoned session must never be captured, got %d captures", active.CapturedCount) - } + assertNoCapturesInActiveCohort(t, "an abandoned session must never be captured") } func TestDogfoodCohort_AutomaticApplyBypassIsNotCaptured(t *testing.T) { @@ -216,13 +222,7 @@ func TestDogfoodCohort_AutomaticApplyBypassIsNotCaptured(t *testing.T) { t.Fatal(err) } - active, err := GetActiveDogfoodCohort(db) - if err != nil { - t.Fatal(err) - } - if active.CapturedCount != 0 { - t.Fatalf("an automatic APPLIED transition must never be captured, got %d captures", active.CapturedCount) - } + assertNoCapturesInActiveCohort(t, "an automatic APPLIED transition must never be captured") } func TestRecordDogfoodFeedback_RejectsUnknownCategory(t *testing.T) { @@ -365,10 +365,7 @@ func TestGetDogfoodReport_ComputesCountsCorrectly(t *testing.T) { } } -func TestGetDogfoodReport_HandlesMissingFeedback(t *testing.T) { - setupTestDB(t) - defer teardownTestDB() - +func seedBasicFiveJobCohort(t *testing.T) *DogfoodCohort { cohort, err := StartDogfoodCohort(db) if err != nil { t.Fatal(err) @@ -377,8 +374,15 @@ func TestGetDogfoodReport_HandlesMissingFeedback(t *testing.T) { id := i + 1 confirmSeededJob(t, id, name) seedDogfoodMetrics(t, id, "greenhouse", 2, 1, 0) - // Deliberately no RecordDogfoodFeedback call -- feedback is optional. } + return cohort +} + +func TestGetDogfoodReport_HandlesMissingFeedback(t *testing.T) { + setupTestDB(t) + defer teardownTestDB() + + cohort := seedBasicFiveJobCohort(t) report, err := GetDogfoodReport(db, cohort.ID) if err != nil { @@ -396,15 +400,7 @@ func TestGetDogfoodReport_HandlesMissingInteractionTiming(t *testing.T) { setupTestDB(t) defer teardownTestDB() - cohort, err := StartDogfoodCohort(db) - if err != nil { - t.Fatal(err) - } - for i, name := range []string{"First", "Second", "Third", "Fourth", "Fifth"} { - id := i + 1 - confirmSeededJob(t, id, name) - seedDogfoodMetrics(t, id, "greenhouse", 2, 1, 0) // no interaction timing recorded - } + cohort := seedBasicFiveJobCohort(t) report, err := GetDogfoodReport(db, cohort.ID) if err != nil { @@ -430,12 +426,13 @@ func TestDogfoodVerdict_ExpectedHumanOnlyQuestionsAreNotFlaggedAsDefects(t *test DogfoodFeedbackNothing, DogfoodFeedbackOneOffQuestion, } - for i, name := range []string{"First", "Second", "Third", "Fourth", "Fifth"} { - id := i + 1 - confirmSeededJob(t, id, name) - seedDogfoodMetrics(t, id, "greenhouse", 4, 2, 0) - if err := RecordDogfoodFeedback(db, itoa(id), feedback[i], nil, ""); err != nil { - t.Fatal(err) + for jobIdx, companyName := range []string{"First", "Second", "Third", "Fourth", "Fifth"} { + jobNum := jobIdx + 1 + confirmSeededJob(t, jobNum, companyName) + seedDogfoodMetrics(t, jobNum, "greenhouse", 4, 2, 0) + category := feedback[jobIdx] + if recErr := RecordDogfoodFeedback(db, itoa(jobNum), category, nil, ""); recErr != nil { + t.Fatal(recErr) } } diff --git a/scripts/test_seo.py b/scripts/test_seo.py new file mode 100755 index 0000000..cb4e086 --- /dev/null +++ b/scripts/test_seo.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +SEO & Search Discoverability Verification Suite for Career Agent Core. +Validates structured data, canonical tags, sitemap, robots.txt, and entity linking. +""" + +import os +import re +import sys +import json +import xml.etree.ElementTree as ET + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DOCS_DIR = os.path.join(REPO_ROOT, "docs") +INDEX_HTML = os.path.join(DOCS_DIR, "index.html") +ROBOTS_TXT = os.path.join(DOCS_DIR, "robots.txt") +SITEMAP_XML = os.path.join(DOCS_DIR, "sitemap.xml") + +def test_seo(): + print("=== Running Career Agent Core SEO Verification ===") + + # 1. Check docs/index.html + assert os.path.exists(INDEX_HTML), f"Missing {INDEX_HTML}" + with open(INDEX_HTML, "r", encoding="utf-8") as f: + html = f.read() + + # Title check + title_match = re.search(r"(.*?)", html, re.IGNORECASE | re.DOTALL) + assert title_match, "Missing tag in index.html" + title = title_match.group(1).strip() + assert "Career Agent Core" in title, f"Title does not mention Career Agent Core: {title}" + print(f" [PASS] HTML <title> exists: {title}") + + # Meta description check + meta_desc_match = re.search( + r'<meta\s+name=["\']description["\']\s+content=["\'](.*?)["\']', + html, + re.IGNORECASE, + ) + assert meta_desc_match, "Missing meta description in index.html" + meta_desc = meta_desc_match.group(1).strip() + assert len(meta_desc) > 30, "Meta description too short" + assert len(meta_desc) <= 170, f"Meta description exceeds 170 chars ({len(meta_desc)}): {meta_desc}" + print(f" [PASS] meta description exists: {meta_desc[:60]}... ({len(meta_desc)} chars)") + + # Canonical URL check + canonical_match = re.search( + r'<link\s+rel=["\']canonical["\']\s+href=["\'](.*?)["\']', + html, + re.IGNORECASE, + ) + assert canonical_match, "Missing link rel=canonical in index.html" + canonical_url = canonical_match.group(1).strip() + expected_canonical = "https://howlcipher.github.io/Career_Agent_Core/" + assert canonical_url == expected_canonical, ( + f"Canonical URL mismatch: expected {expected_canonical}, got {canonical_url}" + ) + print(f" [PASS] Canonical URL matches: {canonical_url}") + + # Heading hierarchy check + h1_matches = re.findall(r"<h1[^>]*>(.*?)</h1>", html, re.IGNORECASE | re.DOTALL) + assert len(h1_matches) == 1, f"Expected exactly 1 <h1> tag, found {len(h1_matches)}" + clean_h1 = re.sub(r"<[^>]+>", "", h1_matches[0]).strip() + print(f" [PASS] Exactly 1 H1 present: {clean_h1}") + + # Open Graph & Twitter Cards + assert 'property="og:title"' in html or "property='og:title'" in html, "Missing og:title" + assert 'property="og:description"' in html or "property='og:description'" in html, "Missing og:description" + assert 'property="og:url"' in html or "property='og:url'" in html, "Missing og:url" + assert 'name="twitter:card"' in html or "name='twitter:card'" in html, "Missing twitter:card" + print(" [PASS] Open Graph and Twitter card tags verified") + + # JSON-LD Schema.org Structured Data + json_ld_matches = re.findall( + r'<script\s+type=["\']application/ld\+json["\']\s*>(.*?)</script>', + html, + re.IGNORECASE | re.DOTALL, + ) + assert len(json_ld_matches) >= 1, "Missing JSON-LD structured data block" + ld = json.loads(json_ld_matches[0].strip()) + assert ld.get("@context") in ("https://schema.org", "http://schema.org"), "Invalid @context" + ld_type = ld.get("@type") + assert ld_type == "SoftwareApplication", ( + f"Expected SoftwareApplication, got {ld_type}" + ) + author = ld.get("author", {}) + expected_author = "William" + " " + "Elias" + assert author.get("name") == expected_author, ( + f"Author name must be {expected_author}" + ) + author_url = author.get("url") + expected_author_url = "https://howlcipher.github.io/william_elias/" + assert author_url == expected_author_url, ( + "Author URL must point to william_elias portfolio" + ) + print(f" [PASS] JSON-LD valid and correctly attributes {expected_author}") + + # Internal entity links + william_url = "https://howlcipher.github.io/william_elias/" + assert william_url in html, f"Missing link to William Elias portfolio ({william_url})" + print(" [PASS] Internal entity link to William Elias verified") + + # 2. Check docs/robots.txt + assert os.path.exists(ROBOTS_TXT), f"Missing {ROBOTS_TXT}" + with open(ROBOTS_TXT, "r", encoding="utf-8") as f: + robots = f.read() + assert "User-agent: *" in robots, "robots.txt missing User-agent: *" + assert "Allow: /" in robots, "robots.txt missing Allow: /" + assert "Sitemap: https://howlcipher.github.io/Career_Agent_Core/sitemap.xml" in robots, ( + "robots.txt missing correct Sitemap directive" + ) + print(" [PASS] docs/robots.txt valid and references sitemap") + + # 3. Check docs/sitemap.xml + assert os.path.exists(SITEMAP_XML), f"Missing {SITEMAP_XML}" + tree = ET.parse(SITEMAP_XML) + root = tree.getroot() + namespace = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"} + locs = [elem.text.strip() for elem in root.findall(".//sm:loc", namespace)] + assert expected_canonical in locs, ( + f"sitemap.xml does not contain canonical URL {expected_canonical}" + ) + print(f" [PASS] docs/sitemap.xml valid and contains canonical URL") + + print("\nAll SEO validations PASSED successfully!\n") + +if __name__ == "__main__": + try: + test_seo() + except AssertionError as e: + print(f"FAIL: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + sys.exit(1)