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)#
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)#
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)#
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
DRAFT → PUBLISHED
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)#
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)#
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)#
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