Browse documentation

Test Specs

RankFlow AI — Citation Network Test Specification

// src/tests/factories/citation-directory.ts

docs/test-specs/TEST-citation-network.md
On this page

Version: 1.0.0
Date: 2026-06-13
Scope: Network Architecture, Directory Registry, Submission Engine, NAP Monitoring, Owned Blog Sites, Success Metrics
Target: 80% citation success rate, 95% NAP consistency
Source Spec: docs/specs/citation-network.md


1. Mock Factories#

1.1 Citation Directory Factory (30 Directories)#

// src/__tests__/factories/citation-directory.ts
import { faker } from "@faker-js/faker";

export const createMockCitationDirectory = (overrides?: Partial<CitationDirectory>) => ({
  id: `dir_${faker.string.nanoid(8)}`,
  name: faker.internet.domainWord(),
  displayName: faker.company.name(),
  domain: faker.internet.domainName(),
  category: faker.helpers.arrayElement(["medical", "general", "local", "maps"]),
  authorityScore: faker.number.int({ min: 20, max: 90 }),
  isActive: true,
  requiresCaptcha: faker.datatype.boolean(),
  requiresPhoneVerify: faker.datatype.boolean(),
  submissionType: faker.helpers.arrayElement(["API", "FORM", "EMAIL"]),
  signupFlow: { steps: [] },
  submissionFields: [],
  rateLimit: {},
  successPatterns: [],
  failurePatterns: [],
  createdAt: new Date(),
  updatedAt: new Date(),
  ...overrides,
});

export const MOCK_CITATION_DIRECTORIES: CitationDirectory[] = [
  // India-specific directories (10)
  { ...createMockCitationDirectory(), name: "justdial", displayName: "Justdial", domain: "justdial.com", category: "medical", submissionType: "FORM", requiresCaptcha: true, requiresPhoneVerify: true },
  { ...createMockCitationDirectory(), name: "practo", displayName: "Practo", domain: "practo.com", category: "medical", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "sulekha", displayName: "Sulekha", domain: "sulekha.com", category: "general", submissionType: "FORM", requiresCaptcha: true, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "lybrate", displayName: "Lybrate", domain: "lybrate.com", category: "medical", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: true },
  { ...createMockCitationDirectory(), name: "1mg", displayName: "1mg", domain: "1mg.com", category: "medical", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "indiamart", displayName: "IndiaMART", domain: "indiamart.com", category: "general", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "medindia", displayName: "Medindia", domain: "medindia.net", category: "medical", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "doctoralia", displayName: "Doctoralia India", domain: "doctoralia.in", category: "medical", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: true },
  { ...createMockCitationDirectory(), name: "clinicspots", displayName: "ClinicSpots", domain: "clinicspots.com", category: "medical", submissionType: "FORM", requiresCaptcha: true, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "sehat", displayName: "Sehat", domain: "sehat.com", category: "medical", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },

  // General directories (10)
  { ...createMockCitationDirectory(), name: "yelp", displayName: "Yelp", domain: "yelp.com", category: "general", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "foursquare", displayName: "Foursquare", domain: "foursquare.com", category: "general", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "hotfrog", displayName: "Hotfrog", domain: "hotfrog.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "brownbook", displayName: "Brownbook", domain: "brownbook.net", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "cylex", displayName: "Cylex", domain: "cylex.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "ezlocal", displayName: "EZlocal", domain: "ezlocal.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "merchantcircle", displayName: "MerchantCircle", domain: "merchantcircle.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: true },
  { ...createMockCitationDirectory(), name: "citysquares", displayName: "CitySquares", domain: "citysquares.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "yalwa", displayName: "Yalwa", domain: "yalwa.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "tupalo", displayName: "Tupalo", domain: "tupalo.com", category: "general", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },

  // Global maps / directories (5)
  { ...createMockCitationDirectory(), name: "bing-places", displayName: "Bing Places", domain: "bingplaces.com", category: "maps", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "apple-maps", displayName: "Apple Maps", domain: "maps.apple.com", category: "maps", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: true },
  { ...createMockCitationDirectory(), name: "tomtom", displayName: "TomTom", domain: "tomtom.com", category: "maps", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "here", displayName: "HERE WeGo", domain: "here.com", category: "maps", submissionType: "API", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "mapquest", displayName: "MapQuest", domain: "mapquest.com", category: "maps", submissionType: "FORM", requiresCaptcha: false, requiresPhoneVerify: false },

  // Owned blog pages (5)
  { ...createMockCitationDirectory(), name: "kerala-health", displayName: "Kerala Health", domain: "kerala-health.rankflow.in", category: "local", submissionType: "OWNED_BLOG", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "medical-guide", displayName: "Medical Guide", domain: "medical-guide.rankflow.in", category: "local", submissionType: "OWNED_BLOG", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "health-tips", displayName: "Health Tips", domain: "health-tips.rankflow.in", category: "local", submissionType: "OWNED_BLOG", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "wellness-blog", displayName: "Wellness Blog", domain: "wellness-blog.rankflow.in", category: "local", submissionType: "OWNED_BLOG", requiresCaptcha: false, requiresPhoneVerify: false },
  { ...createMockCitationDirectory(), name: "care-directory", displayName: "Care Directory", domain: "care-directory.rankflow.in", category: "local", submissionType: "OWNED_BLOG", requiresCaptcha: false, requiresPhoneVerify: false },
];

1.2 NAP Data Factory#

// src/__tests__/factories/nap-data.ts
export const createMockNAP = (overrides?: Partial<NAPData>) => ({
  name: "Dr. Smith Dental Clinic",
  address: "123 Main Street, Kochi, Kerala",
  phone: "+919876543210",
  city: "Kochi",
  email: "contact@drsmithdental.com",
  website: "https://drsmith.rankflow.in",
  category: "Dentists",
  ...overrides,
});

export const createMockNAPVariants = () => ({
  exactMatch: createMockNAP(),
  nameMismatch: createMockNAP({ name: "Dr. Smith Dental Care" }),
  addressMismatch: createMockNAP({ address: "456 Park Avenue, Kochi, Kerala" }),
  phoneMismatch: createMockNAP({ phone: "+919876543211" }),
  allMismatch: createMockNAP({ name: "Different Clinic", address: "789 Other Road", phone: "+910000000000" }),
  notFound: createMockNAP({ name: "", address: "", phone: "" }),
});

1.3 Citation Submission Result Factory#

// src/__tests__/factories/citation-submission.ts
export const createMockSubmissionResult = (overrides?: Partial<SubmissionResult>) => ({
  success: true,
  url: `https://${faker.internet.domainName()}/listing/${faker.string.nanoid(6)}`,
  screenshotUrl: `https://s3.rankflow.in/citations/${faker.string.nanoid(8)}.png`,
  error: undefined,
  requiresManual: false,
  ...overrides,
});

2. Network Architecture Tests#

2.1 Unit Tests#

Test Input Expected Output File
network_composition_count MOCK_CITATION_DIRECTORIES 30 total (10 India + 10 general + 5 maps + 5 owned) src/__tests__/unit/citation/network-composition.test.ts
directory_name_uniqueness All 30 directory name fields No duplicates, all lowercase, no spaces src/__tests__/unit/citation/network-composition.test.ts
directory_category_filter category: "medical" Returns exactly 10 directories src/__tests__/unit/citation/network-composition.test.ts
directory_submission_type_count submissionType: "FORM" 15 form-based directories src/__tests__/unit/citation/network-composition.test.ts
slug_generation_from_name "Dr. Smith Dental Clinic" "dr-smith-dental-clinic" src/__tests__/unit/citation/slug.test.ts
slug_generation_from_displayName "Justdial" "justdial" src/__tests__/unit/citation/slug.test.ts
slug_uniqueness_for_owned_blogs 5 owned blog domains All slugs unique, no collisions src/__tests__/unit/citation/slug.test.ts
authority_score_range All directory authority scores Between 0 and 100 inclusive src/__tests__/unit/citation/network-composition.test.ts

2.2 Integration Tests#

Test Setup Action Assertion File
list_directories_api DB seeded with 30 directories citationRouter.listDirectories() Returns 30 active directories, sorted by category src/__tests__/integration/citation/router.test.ts
filter_directories_by_category DB seeded with mixed categories Query with category: "maps" Returns exactly 5 directories src/__tests__/integration/citation/router.test.ts
inactive_directory_exclusion 1 directory marked isActive: false listDirectories() Returns 29 directories, inactive excluded src/__tests__/integration/citation/router.test.ts

2.3 Success Criteria (Binary)#

  • ARC-01: Network contains exactly 30 directories (10 India-specific + 10 general + 5 global maps + 5 owned blogs)
  • ARC-02: All directory name fields are unique, lowercase, and contain no spaces
  • ARC-03: All directory submissionType values are one of: API, FORM, EMAIL, OWNED_BLOG
  • ARC-04: Slug generation produces unique, URL-safe strings for all 30 directories
  • ARC-05: Authority scores are integers between 0–100 for all directories

2.4 Agent Context (Pre-conditions)#

  • Required DB state: citation_directories table seeded with 30 rows
  • Required env vars: DATABASE_URL
  • Required external mocks: None (pure DB query)

3. Directory Registry Tests#

3.1 Unit Tests#

Test Input Expected Output File
directory_schema_validation Valid CitationDirectory object Passes Prisma schema validation src/__tests__/unit/citation/directory-schema.test.ts
directory_schema_validation_missing_name Object without name field Throws PrismaClientValidationError src/__tests__/unit/citation/directory-schema.test.ts
submission_fields_json_parsing Justdial submission fields JSON Returns array of 8 fields with correct types src/__tests__/unit/citation/directory-schema.test.ts
phone_pattern_validation +919876543210 Passes ^\+91[0-9]{10}$ regex src/__tests__/unit/citation/directory-schema.test.ts
phone_pattern_validation_invalid +12345 Fails ^\+91[0-9]{10}$ regex src/__tests__/unit/citation/directory-schema.test.ts
signup_flow_json_structure Valid signupFlow JSON Contains steps array with url and action src/__tests__/unit/citation/directory-schema.test.ts
rate_limit_default_empty New directory without rateLimit Defaults to {} src/__tests__/unit/citation/directory-schema.test.ts
success_patterns_array Directory with 3 success patterns Returns array of 3 regex strings src/__tests__/unit/citation/directory-schema.test.ts
failure_patterns_array Directory with 2 failure patterns Returns array of 2 regex strings src/__tests__/unit/citation/directory-schema.test.ts

3.2 Integration Tests#

Test Setup Action Assertion File
create_directory_record Valid directory data db.citationDirectory.create() Record exists with correct fields src/__tests__/integration/citation/directory-crud.test.ts
unique_name_constraint Existing directory "justdial" Create another "justdial" Throws unique constraint error src/__tests__/integration/citation/directory-crud.test.ts
update_directory_submission_type Existing FORM directory Update to submissionType: "API" Record updated, returns new type src/__tests__/integration/citation/directory-crud.test.ts
soft_delete_via_isActive Active directory Set isActive: false listDirectories() excludes it src/__tests__/integration/citation/directory-crud.test.ts
query_directory_by_name 30 directories seeded findUnique({ name: "yelp" }) Returns Yelp directory with all fields src/__tests__/integration/citation/directory-crud.test.ts
json_field_submission_fields_query Seeded directory Select submissionFields Returns parsed JSON, not string src/__tests__/integration/citation/directory-crud.test.ts

3.3 Success Criteria (Binary)#

  • REG-01: name field is unique across all 30 directories (DB constraint enforced)
  • REG-02: submissionType is one of API, FORM, EMAIL, OWNED_BLOG for all directories
  • REG-03: All FORM directories have non-empty submissionFields with valid selectors
  • REG-04: All API directories have valid apiEndpoint defined in config
  • REG-05: isActive false excludes directory from submission queue
  • REG-06: JSON fields (signupFlow, submissionFields, rateLimit) parse without error

3.4 Agent Context (Pre-conditions)#

  • Required DB state: citation_directories table with schema migrated
  • Required env vars: DATABASE_URL
  • Required external mocks: None

4. Submission Engine Tests#

4.1 Unit Tests#

Test Input Expected Output File
form_submitter_field_mapping SubmissionData with all fields Correct selector-value mapping for each field src/__tests__/unit/citation/form-submitter.test.ts
form_submitter_field_mapping_missing_optional SubmissionData without website Maps to https://{directorySlug}.rankflow.in src/__tests__/unit/citation/form-submitter.test.ts
form_submitter_captcha_detection Directory with requiresCaptcha: true Calls browser.solveCaptcha() src/__tests__/unit/citation/form-submitter.test.ts
form_submitter_phone_verify_detection Directory with requiresPhoneVerify: true Sets requiresManual: true in result src/__tests__/unit/citation/form-submitter.test.ts
api_submitter_auth_header SubmissionData with credentials Header contains Bearer {password} src/__tests__/unit/citation/api-submitter.test.ts
api_submitter_payload_structure Valid SubmissionData JSON body matches directory API schema src/__tests__/unit/citation/api-submitter.test.ts
api_submitter_error_handling HTTP 400 response Returns { success: false, error: "..." } src/__tests__/unit/citation/api-submitter.test.ts
email_submitter_body_generation SubmissionData Generates email body with all NAP fields src/__tests__/unit/citation/email-submitter.test.ts
submitter_factory_api_type Directory with submissionType: "API" Returns ApiSubmitter instance src/__tests__/unit/citation/submitter-factory.test.ts
submitter_factory_form_type Directory with submissionType: "FORM" Returns FormSubmitter instance src/__tests__/unit/citation/submitter-factory.test.ts
submitter_factory_owned_type Directory with submissionType: "OWNED_BLOG" Returns OwnedBlogSubmitter instance src/__tests__/unit/citation/submitter-factory.test.ts
submitter_factory_unsupported_type Directory with submissionType: "UNKNOWN" Throws UnsupportedSubmitterError src/__tests__/unit/citation/submitter-factory.test.ts
check_exists_true Existing NAP on directory Returns true src/__tests__/unit/citation/submitter-interface.test.ts
check_exists_false New NAP on directory Returns false src/__tests__/unit/citation/submitter-interface.test.ts
submission_result_success Valid submission success: true, url defined, requiresManual: false src/__tests__/unit/citation/submission-result.test.ts
submission_result_failure Error during submission success: false, error defined, requiresManual: true for FORM src/__tests__/unit/citation/submission-result.test.ts
submission_result_screenshot_upload Successful FORM submission screenshotUrl starts with https://s3.rankflow.in src/__tests__/unit/citation/submission-result.test.ts
description_uniqueness_per_directory Same practice, 30 directories 30 unique descriptions, no duplicates src/__tests__/unit/citation/description-gen.test.ts
description_length_limit Directory with maxLength: 500 Description ≤ 500 characters src/__tests__/unit/citation/description-gen.test.ts
description_medical_compliance Dental practice description No banned phrases ("guaranteed cure", "100% success") src/__tests__/unit/citation/description-gen.test.ts

4.2 Integration Tests#

Test Setup Action Assertion File
api_submitter_success_mock MSW mocks POST to practo.com/api/listings apiSubmitter.submit(data) Returns success with listing URL src/__tests__/integration/citation/api-submitter.test.ts
api_submitter_401_failure MSW mocks 401 response apiSubmitter.submit(data) Returns { success: false, error: "Unauthorized" } src/__tests__/integration/citation/api-submitter.test.ts
api_submitter_500_retry MSW mocks 500 then 200 apiSubmitter.submit(data) with retry config Returns success after 1 retry src/__tests__/integration/citation/api-submitter.test.ts
form_submitter_browser_session Mocked HyperbrowserClient formSubmitter.submit(data) Creates session, navigates, fills form, submits, closes src/__tests__/integration/citation/form-submitter.test.ts
form_submitter_captcha_solve Directory with requiresCaptcha: true, mock captcha solver formSubmitter.submit(data) Calls captcha solver before submit src/__tests__/integration/citation/form-submitter.test.ts
form_submitter_success_message_wait Mock browser with .success-message selector formSubmitter.submit(data) Waits up to 10s for success selector src/__tests__/integration/citation/form-submitter.test.ts
form_submitter_timeout_fallback Mock browser, no success message formSubmitter.submit(data) Returns { success: false, requiresManual: true } src/__tests__/integration/citation/form-submitter.test.ts
owned_blog_submitter_db_insert Valid OwnedBlogSite and SubmissionData ownedBlogSubmitter.submit(data) Creates BlogSitePost record with status: "PUBLISHED" src/__tests__/integration/citation/owned-blog-submitter.test.ts
owned_blog_submitter_slug_uniqueness Existing post with slug dr-smith-dental Submit same slug Throws unique constraint error or auto-increments slug src/__tests__/integration/citation/owned-blog-submitter.test.ts
owned_blog_submitter_schema_markup Published post Scrape published page Contains LocalBusiness JSON-LD with correct NAP src/__tests__/integration/citation/owned-blog-submitter.test.ts
submission_queue_bulk_submit 30 directories, 1 practice Promise.all(directories.map(...)) 30 parallel submissions, all tracked src/__tests__/integration/citation/submission-queue.test.ts
submission_queue_concurrency_limit 30 directories, concurrency limit 3 Promise.all with p-limit Max 3 concurrent submissions at any time src/__tests__/integration/citation/submission-queue.test.ts
submission_queue_partial_failure 25 success mocks, 5 failure mocks Submit to all 30 Returns 25 success, 5 failure results src/__tests__/integration/citation/submission-queue.test.ts
submission_rate_limit_respected Directory with rate limit 1/min Submit twice within 1 minute Second submission delayed or queued src/__tests__/integration/citation/submission-queue.test.ts
credentials_retrieval_from_db CitationDirectory with stored credentials submitter.submit(data) Credentials injected from db.directoryCredential src/__tests__/integration/citation/credentials.test.ts
credentials_missing_for_api API directory without credentials submitter.submit(data) Returns { success: false, error: "Credentials required" } src/__tests__/integration/citation/credentials.test.ts

4.3 E2E Tests#

Flow Steps Expected End State File
full_api_submission_flow 1. Seed practice + location 2. Trigger citationBuilder Inngest function 3. Mock API directory responses 4. Assert DB state 30 Citation records created, statuses in SUBMITTED or FAILED, all have napSnapshot e2e/citation/api-submission-flow.spec.ts
full_form_submission_flow 1. Seed practice + location 2. Mock Hyperbrowser sessions 3. Mock CAPTCHA solver 4. Trigger Inngest function 5. Assert screenshots uploaded All FORM submissions have screenshotUrl, success rate ≥ 80% e2e/citation/form-submission-flow.spec.ts
full_owned_blog_submission_flow 1. Seed practice + location 2. Seed 5 owned blog pages 3. Trigger Inngest function 4. Assert DB + rendered pages 5 BlogSitePost records with status: "PUBLISHED", pages render with NAP + schema e2e/citation/owned-blog-submission-flow.spec.ts
submission_with_description_generation 1. Seed practice 2. Trigger generate-descriptions step 3. Assert 30 unique descriptions All descriptions unique, contain practice name, ≤ 500 chars e2e/citation/description-generation.spec.ts
resubmission_after_failure 1. Create failed citation 2. Trigger resubmission event 3. Mock success this time Citation status updated to SUBMITTED, URL populated e2e/citation/resubmission.spec.ts

4.4 Success Criteria (Binary)#

  • SUB-01: Submitter factory returns correct submitter type for API, FORM, EMAIL, OWNED_BLOG
  • SUB-02: API submitter includes Authorization: Bearer header with correct credentials
  • SUB-03: FORM submitter creates browser session, fills all fields, handles CAPTCHA, captures screenshot
  • SUB-04: FORM submitter returns requiresManual: true on timeout or unhandled CAPTCHA
  • SUB-05: Owned blog submitter creates BlogSitePost with unique slug, correct NAP, LocalBusiness schema
  • SUB-06: Description generation produces 30 unique descriptions, no duplicates, all ≤ 500 chars
  • SUB-07: Concurrent submission respects concurrency limit (default: 3)
  • SUB-08: Partial failure is handled gracefully — success results stored, failures retried
  • SUB-09: Overall citation success rate ≥ 80% (24/30 submissions succeed)
  • SUB-10: Rate limits are respected per directory (no 429 errors from real APIs in tests)

4.5 Agent Context (Pre-conditions)#

  • Required DB state: practice, location, citation_directories (30 rows), citation (empty or with test data)
  • Required env vars: DATABASE_URL, HYPERBROWSER_API_KEY, S3_BUCKET, FIRECRAWL_API_KEY
  • Required external mocks: MSW handlers for Practo API, Yelp API, IndiaMART API, S3 upload, Hyperbrowser sessions
  • Required Inngest: INNGEST_EVENT_KEY for workflow triggering

5. NAP Monitoring Tests#

5.1 Unit Tests#

Test Input Expected Output File
fuzzy_match_exact expected: "Dr. Smith Dental Clinic", found: "Dr. Smith Dental Clinic" true src/__tests__/unit/citation/nap-monitor.test.ts
fuzzy_match_case_insensitive expected: "Dr. Smith Dental Clinic", found: "dr smith dental clinic" true src/__tests__/unit/citation/nap-monitor.test.ts
fuzzy_match_with_punctuation expected: "Dr. Smith Dental Clinic", found: "Dr. Smith Dental Clinic, Kochi" true src/__tests__/unit/citation/nap-monitor.test.ts
fuzzy_match_substring expected: "Smith Dental", found: "Dr. Smith Dental Clinic" true src/__tests__/unit/citation/nap-monitor.test.ts
fuzzy_match_mismatch expected: "Dr. Smith Dental Clinic", found: "Dr. Johnson Medical Center" false src/__tests__/unit/citation/nap-monitor.test.ts
normalize_phone_standard +919876543210 919876543210 src/__tests__/unit/citation/nap-monitor.test.ts
normalize_phone_with_spaces +91 98765 43210 919876543210 src/__tests__/unit/citation/nap-monitor.test.ts
normalize_phone_with_dashes +91-98765-43210 919876543210 src/__tests__/unit/citation/nap-monitor.test.ts
normalize_phone_with_country_code 09876543210 9876543210 src/__tests__/unit/citation/nap-monitor.test.ts
phone_match_exact expected: "+919876543210", found: "+919876543210" true src/__tests__/unit/citation/nap-monitor.test.ts
phone_match_normalized expected: "+919876543210", found: "+91 98765 43210" true src/__tests__/unit/citation/nap-monitor.test.ts
phone_match_mismatch expected: "+919876543210", found: "+919876543211" false src/__tests__/unit/citation/nap-monitor.test.ts
nap_result_all_match Exact NAP match { nameMatch: true, addressMatch: true, phoneMatch: true } src/__tests__/unit/citation/nap-monitor.test.ts
nap_result_name_mismatch Name variant { nameMatch: false, addressMatch: true, phoneMatch: true } src/__tests__/unit/citation/nap-monitor.test.ts
nap_result_address_mismatch Address variant { nameMatch: true, addressMatch: false, phoneMatch: true } src/__tests__/unit/citation/nap-monitor.test.ts
nap_result_phone_mismatch Phone variant { nameMatch: true, addressMatch: true, phoneMatch: false } src/__tests__/unit/citation/nap-monitor.test.ts
nap_result_all_mismatch Completely different { nameMatch: false, addressMatch: false, phoneMatch: false } src/__tests__/unit/citation/nap-monitor.test.ts
nap_result_not_found Empty extracted data { nameMatch: false, addressMatch: false, phoneMatch: false, extracted: {} } src/__tests__/unit/citation/nap-monitor.test.ts
match_status_matched All fields match "MATCHED" src/__tests__/unit/citation/nap-monitor.test.ts
match_status_mismatch_name Name differs "MISMATCH_NAME" src/__tests__/unit/citation/nap-monitor.test.ts
match_status_mismatch_address Address differs "MISMATCH_ADDRESS" src/__tests__/unit/citation/nap-monitor.test.ts
match_status_mismatch_phone Phone differs "MISMATCH_PHONE" src/__tests__/unit/citation/nap-monitor.test.ts
match_status_mismatch_all All fields differ "MISMATCH_ALL" src/__tests__/unit/citation/nap-monitor.test.ts
match_status_not_found Empty extracted data "NOT_FOUND" src/__tests__/unit/citation/nap-monitor.test.ts

5.2 Integration Tests#

Test Setup Action Assertion File
verify_nap_with_firecrawl_mock MSW mocks Firecrawl scrape with matching NAP verifyNapConsistency(citation) Returns MATCHED status src/__tests__/integration/citation/nap-verify.test.ts
verify_nap_with_firecrawl_name_mismatch MSW mocks Firecrawl with different name verifyNapConsistency(citation) Returns MISMATCH_NAME, alerts admin src/__tests__/integration/citation/nap-verify.test.ts
verify_nap_with_firecrawl_not_found MSW mocks Firecrawl with empty page verifyNapConsistency(citation) Returns NOT_FOUND, schedules resubmission src/__tests__/integration/citation/nap-verify.test.ts
verify_nap_404_page MSW mocks HTTP 404 verifyNapConsistency(citation) Returns NOT_FOUND, error logged src/__tests__/integration/citation/nap-verify.test.ts
verify_nap_firecrawl_timeout MSW mocks timeout verifyNapConsistency(citation) Returns NOT_FOUND, error logged src/__tests__/integration/citation/nap-verify.test.ts
monthly_nap_check_job 10 citations with mixed statuses napCheckProcessor(job) All 10 checked, mismatches alerted src/__tests__/integration/citation/nap-check-job.test.ts
monthly_nap_check_job_zero_mismatches 10 citations all MATCHED napCheckProcessor(job) No email sent, returns { mismatches: 0 } src/__tests__/integration/citation/nap-check-job.test.ts
monthly_nap_check_job_email_alert 3 mismatches, 7 matches napCheckProcessor(job) Email sent to admin@rankflow.in with list src/__tests__/integration/citation/nap-check-job.test.ts
citation_db_update_after_scan MATCHED result db.citation.update() matchStatus, lastScannedAt, scanResult updated src/__tests__/integration/citation/nap-verify.test.ts
trpc_verify_nap_mutation Authenticated practice user, valid citationId citationRouter.verifyNap({ citationId }) Returns NAP result, updates DB src/__tests__/integration/citation/nap-verify.test.ts
trpc_verify_nap_not_found Invalid citationId citationRouter.verifyNap({ citationId }) Throws TRPCError with code NOT_FOUND src/__tests__/integration/citation/nap-verify.test.ts
trpc_verify_nap_unauthorized User from different practice citationRouter.verifyNap({ citationId }) Throws TRPCError with code FORBIDDEN src/__tests__/integration/citation/nap-verify.test.ts

5.3 E2E Tests#

Flow Steps Expected End State File
nap_monitoring_full_cycle 1. Submit citation 2. Wait for verification event (mocked 7d sleep) 3. Firecrawl scrapes listing 4. Assert NAP match status Citation status VERIFIED or MISMATCH_NAME, lastScannedAt populated e2e/citation/nap-monitoring-cycle.spec.ts
nap_mismatch_alert_flow 1. Create citation with known mismatch 2. Run NAP check job 3. Assert email sent 4. Assert dashboard shows alert Admin email received, practice dashboard shows red alert, update scheduled e2e/citation/nap-mismatch-alert.spec.ts
nap_not_found_resubmission_flow 1. Mark citation as NOT_FOUND 2. Assert resubmission event queued 3. Mock successful resubmission 4. Assert new URL stored New citation record created, old marked REPLACED, success rate maintained e2e/citation/nap-not-found-resubmission.spec.ts

5.4 Success Criteria (Binary)#

  • NAP-01: fuzzyMatch returns true for exact strings, case-insensitive, and punctuation-different strings
  • NAP-02: normalizePhone strips all non-digit characters consistently
  • NAP-03: Phone match uses normalized comparison, not raw string equality
  • NAP-04: MATCHED status requires all three fields (name, address, phone) to match
  • NAP-05: MISMATCH_NAME triggered when name differs but address and phone match
  • NAP-06: MISMATCH_ADDRESS triggered when address differs but name and phone match
  • NAP-07: MISMATCH_PHONE triggered when phone differs but name and address match
  • NAP-08: MISMATCH_ALL triggered when all three fields differ
  • NAP-09: NOT_FOUND triggered when scraped page contains no business name
  • NAP-10: Monthly NAP check job processes all SUBMITTED and VERIFIED citations
  • NAP-11: Email alert sent when ≥1 mismatches detected in monthly check
  • NAP-12: No email sent when zero mismatches detected
  • NAP-13: DB updated with matchStatus, lastScannedAt, scanResult after every check
  • NAP-14: NAP consistency rate ≥ 95% (MATCHED / total live citations)

5.5 Agent Context (Pre-conditions)#

  • Required DB state: citation table with SUBMITTED/VERIFIED records, napSnapshot populated
  • Required env vars: DATABASE_URL, FIRECRAWL_API_KEY, RESEND_API_KEY
  • Required external mocks: MSW handlers for Firecrawl scrape, Resend email API
  • Required job queue: BullMQ processor nap-check registered

6. Owned Blog Sites Tests#

6.1 Unit Tests#

Test Input Expected Output File
owned_blog_site_schema Valid OwnedBlogSite object Passes Prisma schema validation src/__tests__/unit/citation/owned-blog-schema.test.ts
blog_post_slug_generation Title "Dr. Smith Dental Clinic — Kochi" "dr-smith-dental-clinic-kochi" src/__tests__/unit/citation/owned-blog-schema.test.ts
blog_post_slug_uniqueness_constraint Duplicate slug on same blog Prisma unique constraint error on @@unique([blogSiteId, slug]) src/__tests__/unit/citation/owned-blog-schema.test.ts
blog_post_status_transitions DRAFTPUBLISHED Status updated, publishedAt set src/__tests__/unit/citation/owned-blog-schema.test.ts
blog_post_status_failed Publish error status: "FAILED", failedReason populated src/__tests__/unit/citation/owned-blog-schema.test.ts
localbusiness_schema_generation NAP data + URL Valid JSON-LD LocalBusiness schema src/__tests__/unit/citation/owned-blog-schema.test.ts
localbusiness_schema_name_field name: "Dr. Smith Dental Clinic" Schema @context contains https://schema.org src/__tests__/unit/citation/owned-blog-schema.test.ts
localbusiness_schema_address_field Address string Parsed into PostalAddress with streetAddress, addressLocality src/__tests__/unit/citation/owned-blog-schema.test.ts
localbusiness_schema_phone_field +919876543210 Schema telephone field matches input src/__tests__/unit/citation/owned-blog-schema.test.ts
localbusiness_schema_url_field https://drsmith.rankflow.in Schema url matches input src/__tests__/unit/citation/owned-blog-schema.test.ts
owned_blog_submitter_field_value_map SubmissionData Maps to BlogSitePost fields correctly src/__tests__/unit/citation/owned-blog-submitter.test.ts
owned_blog_submitter_backlink_insertion Practice directory profile URL Content contains <a href="{practiceUrl}"> backlink src/__tests__/unit/citation/owned-blog-submitter.test.ts
owned_blog_submitter_description_uniqueness Same practice, 5 owned blogs 5 different descriptions src/__tests__/unit/citation/owned-blog-submitter.test.ts

6.2 Integration Tests#

Test Setup Action Assertion File
create_owned_blog_site Valid domain and niche db.ownedBlogDirectoryProfile.create() Record created with authorityScore: 0, postCount: 0 src/__tests__/integration/citation/owned-blog-crud.test.ts
create_blog_post_with_practice OwnedBlogSite + Practice db.blogSitePost.create() blogSiteId and practiceId linked src/__tests__/integration/citation/owned-blog-crud.test.ts
publish_blog_post_triggers_ISR BlogSitePost with status: "PUBLISHED" Publish event ISR rebuild triggered for blog site domain src/__tests__/integration/citation/owned-blog-publish.test.ts
blog_post_seo_fields_populated Generated content ai.generate({ task: "citation_description" }) seoTitle, seoDescription, focusKeywords populated src/__tests__/integration/citation/owned-blog-seo.test.ts
blog_site_authority_score_update 10 published posts Background job authorityScore incremented per post src/__tests__/integration/citation/owned-blog-authority.test.ts
owned_blog_citation_store_result Successful submit db.citation.create() Citation record linked to BlogSitePost via directoryUrl src/__tests__/integration/citation/owned-blog-citation.test.ts
owned_blog_citation_schema_markup Published page HTTP GET to publishedUrl Response contains application/ld+json with LocalBusiness src/__tests__/integration/citation/owned-blog-citation.test.ts
owned_blog_citation_backlink Published page HTTP GET to publishedUrl HTML contains do-follow link to practice website src/__tests__/integration/citation/owned-blog-citation.test.ts
owned_blog_citation_nap_visible Published page HTTP GET to publishedUrl Business name, address, phone visible in page content src/__tests__/integration/citation/owned-blog-citation.test.ts
trpc_list_owned_blog_posts Authenticated practice user citationRouter.list({ locationId }) Returns citations with directory.name in owned blog set src/__tests__/integration/citation/owned-blog-router.test.ts

6.3 Success Criteria (Binary)#

  • OWN-01: OwnedBlogSite created with unique domain, authorityScore default 0
  • OWN-02: BlogSitePost slug unique per blog site (@@unique([blogSiteId, slug]))
  • OWN-03: Blog post status transitions: DRAFTPUBLISHED (with publishedAt) or FAILED (with failedReason)
  • OWN-04: Published page contains valid LocalBusiness JSON-LD schema with correct NAP
  • OWN-05: Published page contains do-follow backlink to practice website
  • OWN-06: Blog post description is unique per practice per blog site (no duplicate content across 5 owned blogs)
  • OWN-07: SEO fields (seoTitle, seoDescription, focusKeywords) populated for every published post
  • OWN-08: Owned blog submission success rate = 99% (direct DB insert, no external dependencies)
  • OWN-09: ISR re-render triggered automatically on publish
  • OWN-10: postCount and authorityScore updated after each publish

6.4 Agent Context (Pre-conditions)#

  • Required DB state: owned_blog_sites (5 rows), blog_site_posts (empty), practice, location
  • Required env vars: DATABASE_URL, NEXT_PUBLIC_APP_URL
  • Required external mocks: None for DB operations; ISR revalidation mock for publish tests
  • Required file system: blog-sites/ directory for ISR revalidation output

7. E2E: Full Citation Flow#

7.1 Test Flow Definition#

┌─────────────────────────────────────────────────────────────┐
│          E2E Flow: Complete Citation Build (30 Sites)      │
│                                                              │
│  1. Practice Onboarding                                       │
│     └── Create practice + location with valid NAP            │
│                                                              │
│  2. Trigger Citation Builder                                  │
│     └── POST /api/inngest (skill/13-citation-submit)         │
│                                                              │
│  3. Description Generation                                    │
│     └── AI generates 30 unique descriptions                  │
│                                                              │
│  4. Parallel Submission (30 directories)                     │
│     ├── API dirs: Mocked REST calls (10)                     │
│     ├── FORM dirs: Mocked browser automation (10)            │
│     ├── MAP dirs: Mocked API calls (5)                       │
│     └── OWNED dirs: DB insert + ISR (5)                    │
│                                                              │
│  5. Result Storage                                            │
│     └── 30 Citation records in DB                          │
│                                                              │
│  6. Verification (mocked 7-day sleep)                      │
│     └── NAP check on all submitted URLs                    │
│                                                              │
│  7. Success Metrics                                           │
│     └── successRate ≥ 80%, napConsistency ≥ 95%            │
└─────────────────────────────────────────────────────────────┘

7.2 E2E Test Cases#

Flow Steps Expected End State File
complete_citation_build_30_sites 1. Create practice + location 2. Seed 30 directories 3. Trigger citationBuilder Inngest function 4. Mock all external APIs 5. Mock AI description generation 6. Assert DB state after submission 7. Mock 7-day sleep 8. Trigger NAP verification 9. Assert final metrics Citation records: 24+ SUBMITTED/VERIFIED, ≤6 FAILED; NAP consistency ≥ 95%; success rate ≥ 80% e2e/citation/complete-build-flow.spec.ts
complete_citation_build_with_failure_resilience 1. Create practice + location 2. Mock 5 API failures, 3 FORM timeouts, 2 owned blog errors 3. Trigger build 4. Assert 20 success, 10 failure 5. Trigger retry for failures 6. Mock 8 success on retry 7. Assert 28 success, 2 failure Final success rate ≥ 80% (28/30 = 93%) e2e/citation/complete-build-with-retry.spec.ts
complete_citation_build_nap_mismatch_detected 1. Complete build with 30 successes 2. Mock NAP verification: 2 name mismatches, 1 not found 3. Trigger NAP check 4. Assert 27 MATCHED, 2 MISMATCH_NAME, 1 NOT_FOUND 5. Assert email alert sent 6. Assert resubmission queued for NOT_FOUND NAP consistency = 27/30 = 90% (below target, triggers alert); mismatch citations flagged for update e2e/citation/complete-build-nap-mismatch.spec.ts
complete_citation_build_owned_blog_authority 1. Complete build 2. Assert 5 owned blog posts published 3. Assert each post has unique content 4. Assert each page renders with schema 5. Assert backlink present 6. Assert authorityScore updated 5 published posts, 5 unique descriptions, 5 valid schema markups, 5 do-follow backlinks e2e/citation/complete-build-owned-blog.spec.ts
dashboard_citation_listing 1. Complete build 2. Login as practice user 3. Navigate to citation dashboard 4. Assert 30 citations listed 5. Assert statuses color-coded 6. Assert NAP match status visible 7. Click "Verify NAP" on one citation 8. Assert updated status Dashboard shows 30 citations, statuses accurate, NAP verification button works e2e/citation/dashboard-citation-listing.spec.ts
dashboard_citation_retry_failed 1. Build with 5 failures 2. Login as practice user 3. Click "Retry" on failed citation 4. Mock success this time 5. Assert status updated to SUBMITTED Failed citation retried, status updated, success count incremented e2e/citation/dashboard-citation-retry.spec.ts

7.3 Success Criteria (Binary)#

  • E2E-01: Full build flow completes in < 7 days (mocked) from trigger to 30 submissions
  • E2E-02: 30 Citation records created in DB, all linked to correct practice + location
  • E2E-03: Success rate ≥ 80% (≥24 successful submissions out of 30)
  • E2E-04: NAP consistency ≥ 95% (≥95% of live citations show MATCHED)
  • E2E-05: All 30 descriptions are unique (no duplicate content across directories)
  • E2E-06: All FORM submissions have screenshotUrl stored in S3
  • E2E-07: All OWNED_BLOG submissions have publishedUrl and valid LocalBusiness schema
  • E2E-08: Failed submissions have errorMessage and requiresManual flag set correctly
  • E2E-09: Retry mechanism resubmits failed citations and updates status
  • E2E-10: Dashboard accurately reflects all citation statuses and NAP match states
  • E2E-11: NAP mismatch alerts trigger email to admin
  • E2E-12: NOT_FOUND citations trigger automatic resubmission queue

7.4 Agent Context (Pre-conditions)#

  • Required DB state: Full schema migrated, practice, location, citation_directories (30), citation (empty)
  • Required env vars: DATABASE_URL, INNGEST_EVENT_KEY, HYPERBROWSER_API_KEY, S3_BUCKET, FIRECRAWL_API_KEY, RESEND_API_KEY, OPENAI_API_KEY (or AI provider key)
  • Required external mocks: MSW handlers for all 30 directory APIs/Forms, Firecrawl, S3, Resend, AI generation
  • Required services: Inngest dev server, BullMQ worker, Next.js dev server (for E2E)
  • Required test data: Valid NAP data for a dental practice in Kochi, Kerala

8. Success Metrics & Compliance#

8.1 Metrics Calculation Tests#

Test Input Expected Output File
success_rate_calculation 24 success, 6 failure 0.80 (80%) src/__tests__/unit/citation/metrics.test.ts
success_rate_zero_division 0 attempted 0 (no division by zero) src/__tests__/unit/citation/metrics.test.ts
nap_consistency_rate 95 MATCHED, 3 MISMATCH, 2 NOT_FOUND 0.95 (95% — only counts live) src/__tests__/unit/citation/metrics.test.ts
nap_consistency_rate_with_zero_live 0 live citations 1.0 (or N/A) src/__tests__/unit/citation/metrics.test.ts
citation_uptime_rate 28 live, 2 NOT_FOUND out of 30 submitted 0.933 (93.3%) src/__tests__/unit/citation/metrics.test.ts
time_to_30_citations Onboarding date: 2026-01-01, last submission: 2026-01-05 4 days (< 7 days target) src/__tests__/unit/citation/metrics.test.ts
per_directory_success_rate Justdial: 7 success, 3 failure 0.70 (70%) src/__tests__/unit/citation/metrics.test.ts
per_directory_average_time Submission times: [300s, 240s, 360s] 300s (5 min) src/__tests__/unit/citation/metrics.test.ts
per_directory_captcha_rate 10 Justdial submissions, 8 CAPTCHA encountered 0.80 (80%) src/__tests__/unit/citation/metrics.test.ts

8.2 Integration Tests#

Test Setup Action Assertion File
metrics_dashboard_api 30 citations with mixed statuses citationRouter.list({ locationId }) Response includes computed success rate src/__tests__/integration/citation/metrics-api.test.ts
metrics_report_generation Practice with 30 citations Trigger report generation job PDF/CSV contains all per-directory metrics src/__tests__/integration/citation/metrics-report.test.ts
metrics_alert_below_threshold Success rate drops to 75% Background check Alert sent to admin, dashboard shows warning src/__tests__/integration/citation/metrics-alert.test.ts
metrics_alert_nap_below_threshold NAP consistency drops to 90% Background check Alert sent to admin, affected citations flagged src/__tests__/integration/citation/metrics-alert.test.ts
owned_site_da_target 5 owned sites with authority scores Query OwnedBlogSite All sites have authorityScore > 40 (or tracking in place) src/__tests__/integration/citation/metrics-owned-da.test.ts

8.3 Compliance Tests#

Test Input Expected Output File
description_no_guaranteed_cures AI-generated description with "guaranteed cure" Rejected, regeneration triggered src/__tests__/unit/compliance/citation-content.test.ts
description_no_drug_claims AI-generated description with "Amoxicillin treats all infections" Flagged for review, not published src/__tests__/unit/compliance/citation-content.test.ts
citation_no_pii_in_error_logs Submission error with phone number in stack trace Phone number masked in logs src/__tests__/unit/compliance/citation-privacy.test.ts
consent_logging_for_citation_submission Practice user triggers citation build ConsentLog record created with IP + timestamp src/__tests__/integration/compliance/citation-consent.test.ts
dpdpa_data_export_includes_citations Practice requests data export Export includes all Citation records with NAP snapshot src/__tests__/integration/compliance/citation-dpdpa.test.ts
dpdpa_data_deletion_cascade Practice requests deletion All Citation records deleted, directory listings removed (where possible) src/__tests__/integration/compliance/citation-dpdpa.test.ts

8.4 Success Criteria (Binary)#

  • MET-01: Citation success rate calculation: submitted / attempted = ratio 0.0–1.0
  • MET-02: Success rate target: ≥ 80% for any practice with 30 directories attempted
  • MET-03: NAP consistency rate: matched / total live = ratio 0.0–1.0
  • MET-04: NAP consistency target: ≥ 95% for any practice with live citations
  • MET-05: Citation uptime rate: live / total submitted = ratio 0.0–1.0
  • MET-06: Citation uptime target: ≥ 95%
  • MET-07: Time to 30 citations: ≤ 7 days from onboarding trigger
  • MET-08: Per-directory metrics tracked: success rate, avg time, CAPTCHA rate
  • MET-09: Owned blog authority score target: > 40 (tracked, not guaranteed in tests)
  • MET-10: Alert triggered when success rate < 80% or NAP consistency < 95%
  • COM-01: No banned medical phrases in any AI-generated citation description
  • COM-02: No PII (phone, email) in error logs or unmasked stack traces
  • COM-03: Consent logged for every citation submission trigger
  • COM-04: DPDPA data export includes all citation records
  • COM-05: DPDPA deletion cascades to citation records and triggers directory removal

9. Agent Context & Verification#

9.1 Agent Context (Pre-conditions) — Global#

  • Required DB state: All Prisma tables migrated (citation_directories, citations, owned_blog_sites, blog_site_posts, practices, locations)
  • Required env vars:
    • DATABASE_URL — PostgreSQL connection string
    • INNGEST_EVENT_KEY — Inngest event key for workflow triggering
    • INNGEST_SIGNING_KEY — Inngest signing key for function validation
    • HYPERBROWSER_API_KEY — Browser automation API key (mocked in tests)
    • S3_BUCKET — S3 bucket for screenshots (mocked in tests)
    • FIRECRAWL_API_KEY — Web scraping API key (mocked in tests)
    • RESEND_API_KEY — Email API key (mocked in tests)
    • OPENAI_API_KEY — AI generation API key (mocked in tests)
  • Required external mocks (MSW handlers):
    • Practo API POST /api/listings{ listing_url: "..." }
    • Yelp API POST /v3/businesses{ id: "..." }
    • IndiaMART API POST /api/seller{ success: true }
    • Hyperbrowser session creation → { id: "session_123" }
    • Hyperbrowser navigate, fill, click, screenshot → success
    • Hyperbrowser CAPTCHA solve → success (or timeout for failure tests)
    • Firecrawl scrape → { data: { extract: { business_name, address, phone } } }
    • Firecrawl NOT_FOUND → { data: { extract: {} } }
    • S3 upload → { url: "https://s3.rankflow.in/..." }
    • Resend send → { id: "email_123" }
    • AI generate → { content: "unique description..." }
  • Required test utilities:
    • prisma-test-utils for test DB transaction isolation
    • msw for HTTP mock server
    • vitest with jsdom or node environment
    • playwright for E2E browser automation
    • inngest-test-utils for workflow step mocking
  • Required seed data:
    • 30 citation_directories (see Section 1.1 factory)
    • 1 practice (dental clinic in Kochi, Kerala)
    • 1 location with valid NAP (+91 phone, Indian address)
    • 5 owned_blog_sites with .rankflow.in domains

9.2 Verification Commands#

# ─────────────────────────────────────────────
# UNIT TESTS
# ─────────────────────────────────────────────

# Run all citation network unit tests
pnpm test:unit -- src/__tests__/unit/citation/

# Run specific unit test suites
pnpm test:unit -- src/__tests__/unit/citation/slug.test.ts
pnpm test:unit -- src/__tests__/unit/citation/nap-monitor.test.ts
pnpm test:unit -- src/__tests__/unit/citation/form-submitter.test.ts
pnpm test:unit -- src/__tests__/unit/citation/api-submitter.test.ts
pnpm test:unit -- src/__tests__/unit/citation/description-gen.test.ts
pnpm test:unit -- src/__tests__/unit/citation/submitter-factory.test.ts
pnpm test:unit -- src/__tests__/unit/citation/metrics.test.ts
pnpm test:unit -- src/__tests__/unit/compliance/citation-content.test.ts

# ─────────────────────────────────────────────
# INTEGRATION TESTS
# ─────────────────────────────────────────────

# Run all citation network integration tests
pnpm test:integration -- src/__tests__/integration/citation/

# Run specific integration test suites
pnpm test:integration -- src/__tests__/integration/citation/router.test.ts
pnpm test:integration -- src/__tests__/integration/citation/directory-crud.test.ts
pnpm test:integration -- src/__tests__/integration/citation/api-submitter.test.ts
pnpm test:integration -- src/__tests__/integration/citation/form-submitter.test.ts
pnpm test:integration -- src/__tests__/integration/citation/owned-blog-submitter.test.ts
pnpm test:integration -- src/__tests__/integration/citation/nap-verify.test.ts
pnpm test:integration -- src/__tests__/integration/citation/nap-check-job.test.ts
pnpm test:integration -- src/__tests__/integration/citation/submission-queue.test.ts
pnpm test:integration -- src/__tests__/integration/citation/metrics-api.test.ts

# ─────────────────────────────────────────────
# E2E TESTS
# ─────────────────────────────────────────────

# Run all citation network E2E tests
pnpm test:e2e -- e2e/citation/

# Run specific E2E flows
pnpm test:e2e -- e2e/citation/complete-build-flow.spec.ts
pnpm test:e2e -- e2e/citation/complete-build-with-retry.spec.ts
pnpm test:e2e -- e2e/citation/nap-monitoring-cycle.spec.ts
pnpm test:e2e -- e2e/citation/dashboard-citation-listing.spec.ts
pnpm test:e2e -- e2e/citation/owned-blog-submission-flow.spec.ts

# ─────────────────────────────────────────────
# CONTRACT TESTS (External API Mocks)
# ─────────────────────────────────────────────

pnpm test:contract -- src/__tests__/contract/citation/

# ─────────────────────────────────────────────
# COMPLIANCE TESTS
# ─────────────────────────────────────────────

pnpm test:unit -- src/__tests__/unit/compliance/citation-

# ─────────────────────────────────────────────
# COVERAGE REPORT
# ─────────────────────────────────────────────

pnpm test:unit --coverage -- src/__tests__/unit/citation/
pnpm test:integration --coverage -- src/__tests__/integration/citation/

# ─────────────────────────────────────────────
# FULL VERIFICATION (Pre-commit / CI)
# ─────────────────────────────────────────────

pnpm test -- --grep "citation"

9.3 VERIFY.md Template#

Every agent implementing citation network features MUST produce a VERIFY.md in the task workspace:

# Verification Report — Citation Network Task

## Task: [Feature Name]
## Agent: [Role Label]
## Date: [YYYY-MM-DD]

### Tests Run
| Test Suite | Command | Result | Evidence |
|------------|---------|--------|----------|
| Unit | `pnpm test:unit -- ...` | PASS / FAIL | [output file] |
| Integration | `pnpm test:integration -- ...` | PASS / FAIL | [output file] |
| E2E | `pnpm test:e2e -- ...` | PASS / FAIL | [output file] |

### Success Criteria
- [ ] ARC-01: Network contains exactly 30 directories
- [ ] ARC-02: All directory names are unique
- [ ] SUB-09: Citation success rate ≥ 80%
- [ ] NAP-14: NAP consistency rate ≥ 95%
- [ ] [Add relevant criteria from this spec]

### Binary Checklist
- [ ] All unit tests pass
- [ ] All integration tests pass
- [ ] All E2E tests pass (or marked as known limitation)
- [ ] No hardcoded IDs (factories used)
- [ ] All external APIs mocked with MSW
- [ ] No PII in logs or error messages
- [ ] Medical compliance checks pass

### Final Verdict
[ ] PASS — All criteria met, ready to merge
[ ] FAIL — [Explain which criteria failed and why]

End of Citation Network Test Specification — RankFlow AI v1.0.0