Test Specs
RankFlow AI — AI Services Test Specification - [ ] Router resolves the correct default model for every supported task type (12 tasks).
docs/test-specs/TEST-ai-services.mdVersion : 1.0.0
Date : 2026-06-13
Spec : docs/specs/ai-services.md
Scope : src/server/services/ai/
Providers : Anthropic (Claude), OpenAI (GPT-4o)
1. Multi-LLM Router#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
router.selectModel
{ task: "gbp_post" }
Returns claude-haiku (default mapping)
src/__tests__/unit/ai/router.test.ts
router.selectModel.override
{ task: "gbp_post", model: "claude-sonnet" }
Returns claude-sonnet (client override respected)
src/__tests__/unit/ai/router.test.ts
router.selectModel.invalid
{ task: "unknown_task" }
Throws RouterError: Unknown task type
src/__tests__/unit/ai/router.test.ts
router.buildOptions
{ task: "gbp_post", prompt: "Hello", maxTokens: 512 }
Merged options with defaults (temperature: 0.7, maxTokens: 512)
src/__tests__/unit/ai/router.test.ts
router.buildOptions.jsonMode
{ task: "schema_markup", jsonMode: true }
Provider options include response_format: { type: "json_object" }
src/__tests__/unit/ai/router.test.ts
router.result.shape
Valid generate result
Object has text, model, provider, tokensUsed, costUsd, latencyMs
src/__tests__/unit/ai/router.test.ts
router.result.latency
Start = 1000, End = 2450
latencyMs equals 1450 (number)
src/__tests__/unit/ai/router.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
router.generate.happy
Mock Anthropic SDK returns text + usage
ai.generate({ task: "gbp_post", prompt: "Write a post" })
Result.text non-empty, provider="anthropic", costUsd > 0
src/__tests__/integration/ai/router.test.ts
router.generate.openai
Mock OpenAI SDK returns text + usage
ai.generate({ task: "citation_description", prompt: "Write 30 descriptions" })
Result.provider="openai", model="gpt-4o-mini"
src/__tests__/integration/ai/router.test.ts
router.generate.template
DB has practice-specific template
ai.generateWithTemplate(practiceId, "directory_profile_article", vars)
Template loaded from DB, variables substituted, correct model used
src/__tests__/integration/ai/router.test.ts
router.generate.audit
Audit logger spy
ai.generate({ task: "faq" })
logger.info called with event: "ai_generation", all fields present
src/__tests__/integration/ai/router.test.ts
router.generate.trpc
Authenticated tRPC caller
api.content.generate.mutate({ taskType: "GBP_POST", variables: {} })
Returns content object with status: "PENDING_REVIEW", aiGenerated: true, aiTokensUsed > 0
src/__tests__/integration/ai/router.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : PromptTemplate rows for at least one global default and one practice-specific override per task type.
Required env vars : ANTHROPIC_API_KEY (mocked), OPENAI_API_KEY (mocked), DATABASE_URL (test DB).
Required external mocks : Anthropic generateText and OpenAI generateText mocked via msw or vitest spies.
Verification Commands#
# Run router tests
pnpm test:unit -- src/__tests__/unit/ai/router.test.ts
pnpm test:integration -- src/__tests__/integration/ai/router.test.ts
2. Model Configuration#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
config.lookup
modelKey: "claude-sonnet"
Returns provider="anthropic", modelId="claude-sonnet-4-20250514", inputCost=3.00, outputCost=15.00, context=200000
src/__tests__/unit/ai/model-config.test.ts
config.lookup.haiku
modelKey: "claude-haiku"
Returns provider="anthropic", inputCost=0.25, outputCost=1.25
src/__tests__/unit/ai/model-config.test.ts
config.lookup.gpt4o
modelKey: "gpt-4o"
Returns provider="openai", inputCost=2.50, outputCost=10.00, context=128000
src/__tests__/unit/ai/model-config.test.ts
config.lookup.gpt4omini
modelKey: "gpt-4o-mini"
Returns provider="openai", inputCost=0.15, outputCost=0.60
src/__tests__/unit/ai/model-config.test.ts
config.lookup.invalid
modelKey: "gpt-5"
Throws ConfigError: Unsupported model key
src/__tests__/unit/ai/model-config.test.ts
config.cost.calc
inputTokens=1_000_000, outputTokens=500_000, model="claude-sonnet"
costUsd = (1_000_000/1_000_000)*3.00 + (500_000/1_000_000)*15.00 = 10.50
src/__tests__/unit/ai/model-config.test.ts
config.cost.zero
inputTokens=0, outputTokens=0
costUsd = 0.00
src/__tests__/unit/ai/model-config.test.ts
config.context.respect
maxTokens=4096, model="claude-sonnet"
maxTokens capped at model context (200K) or 4096 if smaller; never exceeds context window
src/__tests__/unit/ai/model-config.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
config.cost.rounding
Live cost calculation with fractional tokens
Call calculateCost with 1234 input, 567 output on gpt-4o-mini
costUsd = (1234/1_000_000)*0.15 + (567/1_000_000)*0.60 = 0.0005253 rounded to 6 decimal places
src/__tests__/integration/ai/model-config.test.ts
config.env.missing
ANTHROPIC_API_KEY unset
Attempt to create Anthropic provider
Initialization throws ConfigError: Missing ANTHROPIC_API_KEY
src/__tests__/integration/ai/model-config.test.ts
config.env.missing.openai
OPENAI_API_KEY unset
Attempt to create OpenAI provider
Initialization throws ConfigError: Missing OPENAI_API_KEY
src/__tests__/integration/ai/model-config.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : None (pure config lookup).
Required env vars : ANTHROPIC_API_KEY and OPENAI_API_KEY present or explicitly unset for negative tests.
Required external mocks : None.
Verification Commands#
# Run model config tests
pnpm test:unit -- src/__tests__/unit/ai/model-config.test.ts
pnpm test:integration -- src/__tests__/integration/ai/model-config.test.ts
3. Task-to-Model Mapping#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
mapping.gbp_post
task: "gbp_post"
model: "claude-haiku"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.directory_profile
task: "directory_profile_article"
model: "claude-sonnet"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.faq
task: "faq"
model: "claude-haiku"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.citation
task: "citation_description"
model: "gpt-4o-mini"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.social
task: "social_caption"
model: "gpt-4o-mini"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.review
task: "review_reply"
model: "claude-haiku"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.schema
task: "schema_markup"
model: "claude-sonnet"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.seo_audit
task: "seo_audit"
model: "claude-sonnet"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.meta
task: "meta_description"
model: "gpt-4o-mini"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.evolution
task: "content_evolution"
model: "claude-sonnet"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.report
task: "report_summary"
model: "claude-sonnet"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.keyword
task: "keyword_research"
model: "gpt-4o"
src/__tests__/unit/ai/task-mapping.test.ts
mapping.override.practice
Practice-level default set to claude-sonnet for gbp_post
model: "claude-sonnet" (practice override wins)
src/__tests__/unit/ai/task-mapping.test.ts
mapping.override.client
model: "gpt-4o" in GenerateOptions
model: "gpt-4o" (client override wins over all)
src/__tests__/unit/ai/task-mapping.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
mapping.db.practice
DB row: practiceId="prac_123", taskType="gbp_post", modelConfig={"model":"claude-sonnet"}
ai.generate({ task: "gbp_post", practiceId: "prac_123" })
Anthropic claude-sonnet invoked, not claude-haiku
src/__tests__/integration/ai/task-mapping.test.ts
mapping.db.global
No practice-specific row; global default claude-haiku for gbp_post
ai.generate({ task: "gbp_post", practiceId: "prac_999" })
Anthropic claude-haiku invoked
src/__tests__/integration/ai/task-mapping.test.ts
mapping.db.no_template
No global or practice template for gbp_post
ai.generate({ task: "gbp_post" })
Falls back to hardcoded prompt; model still claude-haiku
src/__tests__/integration/ai/task-mapping.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : PromptTemplate rows with modelConfig overrides for at least one practice and one global default.
Required env vars : DATABASE_URL (test DB).
Required external mocks : Provider SDKs stubbed to verify which model is actually called.
Verification Commands#
# Run task mapping tests
pnpm test:unit -- src/__tests__/unit/ai/task-mapping.test.ts
pnpm test:integration -- src/__tests__/integration/ai/task-mapping.test.ts
4. Prompt Template System#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
template.resolve.practice
practiceId="prac_123", taskType="gbp_post", isDefault=true
Returns practice-specific template with highest version
src/__tests__/unit/ai/templates.test.ts
template.resolve.global
practiceId=null, taskType="gbp_post", isDefault=true
Returns global default template
src/__tests__/unit/ai/templates.test.ts
template.resolve.hardcoded
No DB row for taskType="unknown"
Returns hardcoded fallback prompt object
src/__tests__/unit/ai/templates.test.ts
template.substitute.single
Template: "Hello {{name}}", vars: { name: "World" }
"Hello World"
src/__tests__/unit/ai/templates.test.ts
template.substitute.multiple
Template: "{{city}} {{practiceName}}", vars: { city: "Kochi", practiceName: "Dr. Smith" }
"Kochi Dr. Smith"
src/__tests__/unit/ai/templates.test.ts
template.substitute.missing
Template: "{{city}}", vars: {}
Leaves "{{city}}" in place (or throws TemplateError if strict)
src/__tests__/unit/ai/templates.test.ts
template.substitute.escaped
Template: "{{practiceName}}", vars: { practiceName: "<script>alert(1)</script>" }
Output is HTML-escaped or treated as plain text; no script injection
src/__tests__/unit/ai/templates.test.ts
template.version.latest
Two rows: version 1 (default) and version 2 (default)
Resolves version 2, not version 1
src/__tests__/unit/ai/templates.test.ts
template.ab.select
Variant A performance=4.2, Variant B performance=4.5, threshold=0.3
Auto-promotes B to default; A marked non-default
src/__tests__/unit/ai/templates.test.ts
template.ab.no_promote
Variant A performance=4.2, Variant B performance=4.3, threshold=0.3
No promotion; both remain as-is
src/__tests__/unit/ai/templates.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
template.db.load
Insert practice template with userPromptTemplate: "Write for {{practiceName}}"
ai.generateWithTemplate("prac_123", "gbp_post", { practiceName: "Dr. Smith" })
Provider receives prompt "Write for Dr. Smith"
src/__tests__/integration/ai/templates.test.ts
template.db.schema
Template with outputSchema: { type: "object", required: ["title", "content"] }
Generate with jsonMode: true
Provider receives schema constraint; output is valid JSON
src/__tests__/integration/ai/templates.test.ts
template.db.modelConfig
Template with modelConfig: { provider: "openai", model: "gpt-4o", temperature: 0.3 }
Generate with this template
OpenAI gpt-4o called with temperature: 0.3
src/__tests__/integration/ai/templates.test.ts
template.db.performance
Insert A and B variants with scores
Run promotion job
Winner promoted only if delta ≥ 0.3
src/__tests__/integration/ai/templates.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : PromptTemplate rows with practice-specific, global, A/B variants, and versioned records.
Required env vars : DATABASE_URL (test DB).
Required external mocks : Provider SDKs to inspect the exact prompt and options sent.
Verification Commands#
# Run template system tests
pnpm test:unit -- src/__tests__/unit/ai/templates.test.ts
pnpm test:integration -- src/__tests__/integration/ai/templates.test.ts
5. Cost Tracking#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
cost.calc.claude_sonnet
inputTokens=1000, outputTokens=500, model="claude-sonnet"
costUsd = (1000/1e6)*3.00 + (500/1e6)*15.00 = 0.0105
src/__tests__/unit/ai/cost-tracking.test.ts
cost.calc.gpt4o_mini
inputTokens=2000, outputTokens=1000, model="gpt-4o-mini"
costUsd = (2000/1e6)*0.15 + (1000/1e6)*0.60 = 0.0009
src/__tests__/unit/ai/cost-tracking.test.ts
cost.log.fields
Valid generation result
logger.info payload contains event, task, model, provider, tokensUsed, inputTokens, outputTokens, costUsd, latencyMs, practiceId, userId, requestId
src/__tests__/unit/ai/cost-tracking.test.ts
cost.aggregate.practice
3 generations for practiceId="prac_123"
Aggregation query returns total cost, total tokens, count
src/__tests__/unit/ai/cost-tracking.test.ts
cost.aggregate.task
Generations for task="gbp_post" and task="faq"
Grouped by task type with correct subtotals
src/__tests__/unit/ai/cost-tracking.test.ts
cost.aggregate.model
Generations across Claude and OpenAI models
Grouped by provider with correct split
src/__tests__/unit/ai/cost-tracking.test.ts
cost.aggregate.daily
Generations on 2025-01-15 and 2025-01-16
Grouped by day with correct daily burn rate
src/__tests__/unit/ai/cost-tracking.test.ts
cost.alert.daily50
practiceId="prac_123" exceeds $50 in a day
Alert emitted: alert: "practice_daily_threshold", threshold: 50
src/__tests__/unit/ai/cost-tracking.test.ts
cost.alert.monthly500
practiceId="prac_123" exceeds $500 in a month
Alert emitted: alert: "practice_monthly_threshold", threshold: 500
src/__tests__/unit/ai/cost-tracking.test.ts
cost.alert.system1000
System-wide exceeds $1000 in a day
Alert emitted: alert: "system_daily_threshold", threshold: 1000, action="throttle"
src/__tests__/unit/ai/cost-tracking.test.ts
cost.alert.no_double
Same threshold already alerted today
No duplicate alert sent
src/__tests__/unit/ai/cost-tracking.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
cost.trpc.response
Call api.content.generate
Inspect response payload
Response contains costUsd, aiTokensUsed, generationTimeMs fields
src/__tests__/integration/ai/cost-tracking.test.ts
cost.db.persist
Generate content via tRPC
Query Content table
costUsd, aiTokensUsed, aiProvider, aiModel persisted in DB
src/__tests__/integration/ai/cost-tracking.test.ts
cost.alert.webhook
Alert threshold crossed
Trigger cost alert
Webhook/email sent to admin with practice/system context
src/__tests__/integration/ai/cost-tracking.test.ts
cost.throttle.action
System-wide alert triggered
Attempt non-essential generation
Non-essential tasks (e.g., social_caption) are throttled; essential tasks (e.g., review_reply) still allowed
src/__tests__/integration/ai/cost-tracking.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Content records with costUsd, aiTokensUsed, aiProvider, aiModel, createdAt for aggregation queries.
Required env vars : ADMIN_ALERT_WEBHOOK_URL (mocked), DATABASE_URL (test DB).
Required external mocks : Alert delivery endpoint (webhook/Resend) mocked.
Verification Commands#
# Run cost tracking tests
pnpm test:unit -- src/__tests__/unit/ai/cost-tracking.test.ts
pnpm test:integration -- src/__tests__/integration/ai/cost-tracking.test.ts
6. Fallback Chain#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
fallback.claude_sonnet
Primary: claude-sonnet fails
Fallback: gpt-4o
src/__tests__/unit/ai/fallback.test.ts
fallback.claude_haiku
Primary: claude-haiku fails
Fallback: gpt-4o-mini
src/__tests__/unit/ai/fallback.test.ts
fallback.gpt4o
Primary: gpt-4o fails
Fallback: claude-sonnet
src/__tests__/unit/ai/fallback.test.ts
fallback.gpt4o_mini
Primary: gpt-4o-mini fails
Fallback: claude-haiku
src/__tests__/unit/ai/fallback.test.ts
fallback.all_fail
Primary and fallback both fail
Throws FallbackExhaustedError with both error messages
src/__tests__/unit/ai/fallback.test.ts
fallback.log
Fallback triggered
logger.info called with from, to, error fields
src/__tests__/unit/ai/fallback.test.ts
fallback.circuit.open
5 failures in 60 seconds
Circuit breaker opens; next call throws CircuitOpenError immediately
src/__tests__/unit/ai/fallback.test.ts
fallback.circuit.half_open
Circuit open, then 1 success
Half-open: allows exactly 1 test request
src/__tests__/unit/ai/fallback.test.ts
fallback.circuit.closed
3 consecutive successes in half-open
Circuit closes; normal flow resumes
src/__tests__/unit/ai/fallback.test.ts
fallback.circuit.reset
Circuit open, 30 seconds pass
Auto-reset to half-open after 30s
src/__tests__/unit/ai/fallback.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
fallback.primary_timeout
Anthropic SDK times out after 30s
ai.generate({ task: "directory_profile_article" })
Falls back to OpenAI gpt-4o; result is valid content
src/__tests__/integration/ai/fallback.test.ts
fallback.primary_500
Anthropic SDK returns HTTP 500
ai.generate({ task: "gbp_post" })
Falls back to OpenAI gpt-4o-mini; result is valid content
src/__tests__/integration/ai/fallback.test.ts
fallback.primary_rate_limit
Anthropic SDK returns HTTP 429
ai.generate({ task: "schema_markup" })
Falls back to OpenAI gpt-4o; result is valid JSON-LD
src/__tests__/integration/ai/fallback.test.ts
fallback.secondary_fail
Both Anthropic and OpenAI fail
ai.generate({ task: "faq" })
Throws FallbackExhaustedError; logs incident; alert sent to admin
src/__tests__/integration/ai/fallback.test.ts
fallback.circuit_integration
Rapid-fire 5 failing requests
6th request
6th request immediately rejected with CircuitOpenError (no provider call)
src/__tests__/integration/ai/fallback.test.ts
fallback.circuit_recovery
Circuit open, wait 30s, then 1 success, then 2 more
4th request after 3 successes
4th request succeeds normally; circuit is CLOSED
src/__tests__/integration/ai/fallback.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : Minimal PromptTemplate rows so generation can proceed.
Required env vars : ANTHROPIC_API_KEY, OPENAI_API_KEY (both mocked).
Required external mocks : Anthropic and OpenAI generateText mocked with configurable failure modes (timeout, 500, 429, success).
Verification Commands#
# Run fallback chain tests
pnpm test:unit -- src/__tests__/unit/ai/fallback.test.ts
pnpm test:integration -- src/__tests__/integration/ai/fallback.test.ts
7. Provider Integrations#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
provider.anthropic.create
apiKey provided
createAnthropic returns valid client
src/__tests__/unit/ai/providers.test.ts
provider.anthropic.generate
model="claude-sonnet-4-20250514", prompt="Hello"
generateText returns { text, usage: { promptTokens, completionTokens } }
src/__tests__/unit/ai/providers.test.ts
provider.anthropic.system
system: "You are a doctor"
generateText receives system parameter
src/__tests__/unit/ai/providers.test.ts
provider.anthropic.maxTokens
maxTokens: 2048
generateText receives maxTokens: 2048
src/__tests__/unit/ai/providers.test.ts
provider.anthropic.temperature
temperature: 0.3
generateText receives temperature: 0.3
src/__tests__/unit/ai/providers.test.ts
provider.openai.create
apiKey provided
createOpenAI returns valid client
src/__tests__/unit/ai/providers.test.ts
provider.openai.generate
model="gpt-4o", prompt="Hello"
generateText returns { text, usage: { promptTokens, completionTokens } }
src/__tests__/unit/ai/providers.test.ts
provider.openai.jsonMode
jsonMode: true
generateText receives response_format: { type: "json_object" }
src/__tests__/unit/ai/providers.test.ts
provider.openai.system
system: "You are a dentist"
generateText receives system parameter
src/__tests__/unit/ai/providers.test.ts
provider.unsupported
provider: "gemini"
Throws ProviderError: Unsupported provider
src/__tests__/unit/ai/providers.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
provider.anthropic.mock
Mock Anthropic generateText with msw
generateWithClaude("claude-sonnet-4-20250514", options)
Result matches mock response shape; cost and latency calculated
src/__tests__/integration/ai/providers.test.ts
provider.openai.mock
Mock OpenAI generateText with msw
generateWithOpenAI("gpt-4o", options)
Result matches mock response shape; cost and latency calculated
src/__tests__/integration/ai/providers.test.ts
provider.anthropic.timeout
Mock Anthropic delay > 30s
generateWithClaude(...) with timeout config
Throws ProviderError: Request timeout or falls back
src/__tests__/integration/ai/providers.test.ts
provider.openai.rate_limit
Mock OpenAI returns 429 with retry-after
generateWithOpenAI(...)
Waits for retry-after header or falls back immediately
src/__tests__/integration/ai/providers.test.ts
provider.anthropic.malformed
Mock Anthropic returns 200 with missing usage
generateWithClaude(...)
Throws ProviderError: Invalid response shape or estimates tokens
src/__tests__/integration/ai/providers.test.ts
provider.openai.malformed
Mock OpenAI returns 200 with missing usage
generateWithOpenAI(...)
Throws ProviderError: Invalid response shape or estimates tokens
src/__tests__/integration/ai/providers.test.ts
provider.streaming.anthropic
Mock streamText with chunked chunks
streamText({ model: anthropic("claude-haiku"), ... })
Returns readable stream; chunks contain text deltas
src/__tests__/integration/ai/providers.test.ts
provider.streaming.openai
Mock streamText with chunked chunks
streamText({ model: openai("gpt-4o-mini"), ... })
Returns readable stream; chunks contain text deltas
src/__tests__/integration/ai/providers.test.ts
Mock Responses#
Anthropic Success Mock
{
"id": "msg_01XgYxVjCJ8Kz3vRq9LmNpQ2",
"type": "message",
"role": "assistant",
"content": [
{ "type": "text", "text": "Discover the latest in dental implant technology at Dr. Smith Dental Clinic. Schedule your consultation today!" }
],
"model": "claude-sonnet-4-20250514",
"usage": {
"input_tokens": 120,
"output_tokens": 45
}
}
Anthropic Error Mock (500)
{
"type": "error",
"error": {
"type": "api_error",
"message": "Internal server error"
}
}
Anthropic Error Mock (429)
{
"type": "error",
"error": {
"type": "rate_limit_error",
"message": "Rate limit exceeded"
}
}
OpenAI Success Mock
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Best dentist in Kochi | Dr. Smith Dental Clinic offers top-rated teeth whitening, root canal, and braces treatments."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 85,
"completion_tokens": 32,
"total_tokens": 117
}
}
OpenAI Error Mock (500)
{
"error": {
"message": "The server had an error while processing your request.",
"type": "server_error",
"param": null,
"code": null
}
}
OpenAI Error Mock (429)
{
"error": {
"message": "Rate limit reached",
"type": "requests",
"param": null,
"code": "rate_limit_exceeded"
}
}
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : None (provider-level tests).
Required env vars : ANTHROPIC_API_KEY (mocked), OPENAI_API_KEY (mocked).
Required external mocks : msw handlers for https://api.anthropic.com/v1/messages and https://api.openai.com/v1/chat/completions returning the mock responses above.
Verification Commands#
# Run provider integration tests
pnpm test:unit -- src/__tests__/unit/ai/providers.test.ts
pnpm test:integration -- src/__tests__/integration/ai/providers.test.ts
pnpm test:contract -- src/__tests__/contract/ai/providers.test.ts
8. Content Types#
Test Plan & Verification#
Unit Tests#
Test
Input
Expected Output
File
content.gbp.length
Prompt: "Dr. Smith Dental Clinic, Kochi, teeth whitening"
Generated text length between 150 and 300 characters
src/__tests__/unit/ai/content-types.test.ts
content.gbp.cta
Prompt for GBP post
Text contains a call-to-action ("Schedule", "Call", "Visit", "Book")
src/__tests__/unit/ai/content-types.test.ts
content.landing.length
Prompt: "best dentist in Kochi, 800 words"
Generated text length between 500 and 1000 words
src/__tests__/unit/ai/content-types.test.ts
content.landing.keyword_density
Prompt with target keyword
Keyword density < 2% in output text
src/__tests__/unit/ai/content-types.test.ts
content.review.empathy
Negative review input
Reply contains apologetic or empathetic language
src/__tests__/unit/ai/content-types.test.ts
content.review.no_phi
Review with patient name "John Doe"
Reply does not contain "John Doe" or any patient name
src/__tests__/unit/ai/content-types.test.ts
content.citation.unique
Same inputs, 2 calls
Two outputs are not identical (uniqueness)
src/__tests__/unit/ai/content-types.test.ts
content.citation.length
Prompt: "Dr. Smith, Kochi, dental services, Google"
Output length between 100 and 200 words
src/__tests__/unit/ai/content-types.test.ts
content.social.platform
platform: "instagram"
Output respects Instagram character limit and hashtag rules
src/__tests__/unit/ai/content-types.test.ts
content.schema.valid_json
Prompt: "DentalClinic, Kochi, teeth whitening"
Output is valid JSON-LD string; parses with JSON.parse
src/__tests__/unit/ai/content-types.test.ts
content.schema.schema_org
Valid JSON-LD output
Contains @context: "https://schema.org" and @type
src/__tests__/unit/ai/content-types.test.ts
content.faq.count
Prompt: "dental clinic, common questions"
Output contains 5–10 Q&A pairs
src/__tests__/unit/ai/content-types.test.ts
content.faq.accuracy
FAQ about root canals
Does not contain medically incorrect claims (e.g., "root canals are 100% painless")
src/__tests__/unit/ai/content-types.test.ts
Integration Tests#
Test
Setup
Action
Assertion
File
content.gbp.medical_filter
Mock response containing "100% cure guaranteed"
ai.generate({ task: "gbp_post" })
Medical safety filter rejects content; triggers regeneration or returns error
src/__tests__/integration/ai/content-types.test.ts
content.landing.originality
Mock response with plagiarized text
ai.generate({ task: "directory_profile_article" })
Plagiarism/similarity check flags content; triggers regeneration or error
src/__tests__/integration/ai/content-types.test.ts
content.review.hipaa
Mock reply containing patient name
ai.generate({ task: "review_reply" })
HIPAA filter strips patient name; sanitized reply returned
src/__tests__/integration/ai/content-types.test.ts
content.schema.google_validate
Generated JSON-LD
Validate against Google Rich Results Test schema
No critical errors; MedicalBusiness or LocalBusiness type present
src/__tests__/integration/ai/content-types.test.ts
content.faq.disclaimer
FAQ with medical advice
Generated output
Contains disclaimer if needed (e.g., "Consult your dentist for personalized advice")
src/__tests__/integration/ai/content-types.test.ts
content.citation.duplicate
Same practice, same directory, 2nd generation
ai.generate({ task: "citation_description" })
Second output is different from first (no duplicate descriptions)
src/__tests__/integration/ai/content-types.test.ts
content.social.hashtag
platform: "twitter"
Generated caption
Respects 280-character limit; hashtag count ≤ 3 for Twitter
src/__tests__/integration/ai/content-types.test.ts
Compliance & Medical Safety Tests#
Test
Trigger
Check
Failure Action
File
compliance.no_guarantees
Any AI content generation
Regex scan for banned phrases: 100% cure, guaranteed results, permanent fix, no side effects
Reject + regenerate; log compliance event
src/__tests__/compliance/ai-safety.test.ts
compliance.no_drug_claims
Any AI content generation
Entity recognition for drug names + claim verbs
Flag for human review; block auto-publish
src/__tests__/compliance/ai-safety.test.ts
compliance.hipaa_safe
review_reply generation
No patient names, phone numbers, appointment dates, or medical record numbers in output
Strip + regenerate; alert if 2nd attempt also fails
src/__tests__/compliance/ai-safety.test.ts
compliance.medical_disclaimer
faq or gbp_post with medical advice
Output contains disclaimer or is tagged needs_disclaimer
Add disclaimer before publish; do not publish without it
src/__tests__/compliance/ai-safety.test.ts
compliance.schema_valid
schema_markup generation
JSON-LD parses and contains required Schema.org properties
Fallback to minimal valid schema; alert admin
src/__tests__/compliance/ai-safety.test.ts
compliance.consent_log
Any AI-generated content approved for publish
DB record in ConsentLog with action: "ai_content_approved", ip, timestamp, userId
Block progression if log missing
src/__tests__/compliance/ai-safety.test.ts
Success Criteria (Binary)#
Agent Context (Pre-conditions)#
Required DB state : PromptTemplate rows for each content type; ConsentLog table empty before approval tests.
Required env vars : ANTHROPIC_API_KEY, OPENAI_API_KEY (mocked); DATABASE_URL (test DB).
Required external mocks : Provider SDKs returning content that passes and fails compliance checks.
Required fixtures : Pre-defined mock responses that contain banned phrases and PHI for negative tests.
Verification Commands#
# Run content type tests
pnpm test:unit -- src/__tests__/unit/ai/content-types.test.ts
pnpm test:integration -- src/__tests__/integration/ai/content-types.test.ts
pnpm test:compliance -- src/__tests__/compliance/ai-safety.test.ts
9. Verification Commands#
Full AI Services Test Suite#
# ───────────────────────────────────────────────
# 1. Unit Tests (fast, isolated)
# ───────────────────────────────────────────────
pnpm test:unit -- src/__tests__/unit/ai/
# Individual unit test files
pnpm test:unit -- src/__tests__/unit/ai/router.test.ts
pnpm test:unit -- src/__tests__/unit/ai/model-config.test.ts
pnpm test:unit -- src/__tests__/unit/ai/task-mapping.test.ts
pnpm test:unit -- src/__tests__/unit/ai/templates.test.ts
pnpm test:unit -- src/__tests__/unit/ai/cost-tracking.test.ts
pnpm test:unit -- src/__tests__/unit/ai/fallback.test.ts
pnpm test:unit -- src/__tests__/unit/ai/providers.test.ts
pnpm test:unit -- src/__tests__/unit/ai/content-types.test.ts
# ───────────────────────────────────────────────
# 2. Integration Tests (DB + mocked providers)
# ───────────────────────────────────────────────
pnpm test:integration -- src/__tests__/integration/ai/
# Individual integration test files
pnpm test:integration -- src/__tests__/integration/ai/router.test.ts
pnpm test:integration -- src/__tests__/integration/ai/model-config.test.ts
pnpm test:integration -- src/__tests__/integration/ai/task-mapping.test.ts
pnpm test:integration -- src/__tests__/integration/ai/templates.test.ts
pnpm test:integration -- src/__tests__/integration/ai/cost-tracking.test.ts
pnpm test:integration -- src/__tests__/integration/ai/fallback.test.ts
pnpm test:integration -- src/__tests__/integration/ai/providers.test.ts
pnpm test:integration -- src/__tests__/integration/ai/content-types.test.ts
# ───────────────────────────────────────────────
# 3. Contract Tests (provider API shapes)
# ───────────────────────────────────────────────
pnpm test:contract -- src/__tests__/contract/ai/providers.test.ts
# ───────────────────────────────────────────────
# 4. Compliance Tests (medical safety)
# ───────────────────────────────────────────────
pnpm test:compliance -- src/__tests__/compliance/ai-safety.test.ts
# ───────────────────────────────────────────────
# 5. Full AI Services Suite (all categories)
# ───────────────────────────────────────────────
pnpm test -- src/__tests__/unit/ai/ src/__tests__/integration/ai/ src/__tests__/compliance/ai-safety.test.ts
Coverage Target#
# Run with coverage report
pnpm test:unit --coverage -- src/__tests__/unit/ai/
Metric
Target
Fail If Below
Statements
≥ 90%
85%
Branches
≥ 85%
80%
Functions
≥ 90%
85%
Lines
≥ 90%
85%
Pre-commit Gate#
# All AI tests must pass before any commit touching src/server/services/ai/
pnpm test:unit -- src/__tests__/unit/ai/ && pnpm test:integration -- src/__tests__/integration/ai/
End of AI Services Test Specification — RankFlow AI v1.0.0