forked from sagnik/Project_Velocity
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#41
974 lines
34 KiB
Swift
974 lines
34 KiB
Swift
import SwiftUI
|
|
import XCTest
|
|
@testable import velocity
|
|
|
|
final class VelocitySmokeTests: XCTestCase {
|
|
@MainActor
|
|
func testContentViewCanBeConstructed() {
|
|
let view = ContentView()
|
|
XCTAssertNotNil(view)
|
|
}
|
|
|
|
func testAppSectionsRemainStable() {
|
|
XCTAssertEqual(
|
|
AppSection.allCases.map(\.rawValue),
|
|
[
|
|
"Dashboard",
|
|
"Clients",
|
|
"Imports",
|
|
"Communications",
|
|
"Calendar",
|
|
"Oracle",
|
|
"Sentinel",
|
|
"Inventory",
|
|
"Settings",
|
|
]
|
|
)
|
|
}
|
|
|
|
func testAppSectionDisplayTitlesMatchProductionScope() {
|
|
XCTAssertEqual(
|
|
AppSection.allCases.map(\.displayTitle),
|
|
[
|
|
"Dashboard",
|
|
"Clients",
|
|
"Imports",
|
|
"Communications",
|
|
"Calendar",
|
|
"Oracle",
|
|
"Operator Posture",
|
|
"Inventory",
|
|
"Settings",
|
|
]
|
|
)
|
|
}
|
|
|
|
func testAppConfigParsesExplicitValuesAndRejectsPlaceholders() {
|
|
XCTAssertEqual(
|
|
AppConfig.parsedValue(from: ["BASE_URL": " https://velocity.desineuron.in/api "], key: "BASE_URL"),
|
|
"https://velocity.desineuron.in/api"
|
|
)
|
|
XCTAssertNil(
|
|
AppConfig.parsedValue(from: ["BASE_URL": "$(BASE_URL)"], key: "BASE_URL")
|
|
)
|
|
XCTAssertNil(
|
|
AppConfig.parsedValue(from: ["BASE_URL": " "], key: "BASE_URL")
|
|
)
|
|
XCTAssertNil(
|
|
AppConfig.parsedValue(from: nil, key: "BASE_URL")
|
|
)
|
|
}
|
|
|
|
func testAuthModeDescriptionOnlyReportsConfiguredCredentials() {
|
|
XCTAssertEqual(
|
|
AppConfig.authModeDescription(bearerToken: "token", email: nil, password: nil),
|
|
"Bearer token"
|
|
)
|
|
XCTAssertEqual(
|
|
AppConfig.authModeDescription(bearerToken: nil, email: "user@example.com", password: "secret"),
|
|
"Email/password"
|
|
)
|
|
XCTAssertEqual(
|
|
AppConfig.authModeDescription(bearerToken: nil, email: "user@example.com", password: nil),
|
|
"Credentials required"
|
|
)
|
|
}
|
|
|
|
func testSessionDraftRejectsInsecureOrIncompleteConfiguration() {
|
|
let insecureDraft = SessionConfigurationDraft(
|
|
baseURL: "http://velocity.desineuron.in/api",
|
|
dreamWeaverBaseURL: "",
|
|
dreamWeaverAPIKey: "",
|
|
authMode: .emailPassword,
|
|
email: "operator@desineuron.in",
|
|
password: "secret",
|
|
bearerToken: "",
|
|
existingDreamWeaverAPIKeyAvailable: false,
|
|
existingPasswordAvailable: false,
|
|
existingBearerTokenAvailable: false,
|
|
baselineEmail: nil
|
|
)
|
|
XCTAssertFalse(insecureDraft.validationErrors().isEmpty)
|
|
|
|
let missingPasswordDraft = SessionConfigurationDraft(
|
|
baseURL: "https://velocity.desineuron.in/api",
|
|
dreamWeaverBaseURL: "",
|
|
dreamWeaverAPIKey: "",
|
|
authMode: .emailPassword,
|
|
email: "operator@desineuron.in",
|
|
password: "",
|
|
bearerToken: "",
|
|
existingDreamWeaverAPIKeyAvailable: false,
|
|
existingPasswordAvailable: false,
|
|
existingBearerTokenAvailable: false,
|
|
baselineEmail: nil
|
|
)
|
|
XCTAssertTrue(
|
|
missingPasswordDraft.validationErrors().contains("Password is required for email/password login.")
|
|
)
|
|
}
|
|
|
|
func testSessionDraftNormalizesHttpsOriginAndReusesExistingSecretsSafely() {
|
|
let draft = SessionConfigurationDraft(
|
|
baseURL: " https://Velocity.DesiNeuron.in/api/ ",
|
|
dreamWeaverBaseURL: " https://dreamweaver.DesiNeuron.in/ ",
|
|
dreamWeaverAPIKey: "",
|
|
authMode: .emailPassword,
|
|
email: "operator@desineuron.in",
|
|
password: "",
|
|
bearerToken: "",
|
|
existingDreamWeaverAPIKeyAvailable: true,
|
|
existingPasswordAvailable: true,
|
|
existingBearerTokenAvailable: false,
|
|
baselineEmail: "operator@desineuron.in"
|
|
)
|
|
|
|
XCTAssertEqual(draft.normalizedBaseURL, "https://velocity.desineuron.in/api")
|
|
XCTAssertEqual(draft.normalizedDreamWeaverBaseURL, "https://dreamweaver.desineuron.in")
|
|
XCTAssertEqual(
|
|
draft.resolvedPassword(existingPassword: "stored-secret"),
|
|
"stored-secret"
|
|
)
|
|
XCTAssertEqual(
|
|
draft.resolvedEmail(existingEmail: nil),
|
|
"operator@desineuron.in"
|
|
)
|
|
XCTAssertEqual(
|
|
draft.resolvedDreamWeaverAPIKey(existingKey: "stored-gateway-key"),
|
|
"stored-gateway-key"
|
|
)
|
|
}
|
|
|
|
func testSessionDraftRejectsInsecureDreamWeaverEndpoint() {
|
|
let draft = SessionConfigurationDraft(
|
|
baseURL: "https://velocity.desineuron.in/api",
|
|
dreamWeaverBaseURL: "http://54.172.172.2:8082",
|
|
dreamWeaverAPIKey: "",
|
|
authMode: .bearerToken,
|
|
email: "",
|
|
password: "",
|
|
bearerToken: "token",
|
|
existingDreamWeaverAPIKeyAvailable: false,
|
|
existingPasswordAvailable: false,
|
|
existingBearerTokenAvailable: false,
|
|
baselineEmail: nil
|
|
)
|
|
|
|
XCTAssertTrue(
|
|
draft.validationErrors().contains(
|
|
"Dream Weaver endpoint must be an HTTPS origin like https://dreamweaver.desineuron.in."
|
|
)
|
|
)
|
|
}
|
|
|
|
func testAppSessionConfigurationReflectsDreamWeaverGatewayKeyState() {
|
|
let configured = AppSessionConfiguration(
|
|
baseURL: "https://velocity.desineuron.in/api",
|
|
dreamWeaverBaseURL: "https://dreamweaver.desineuron.in",
|
|
usesDedicatedDreamWeaverBaseURL: true,
|
|
hasDreamWeaverAPIKey: true,
|
|
email: "operator@desineuron.in",
|
|
hasPassword: true,
|
|
hasBearerToken: false,
|
|
source: .secureDeviceStorage
|
|
)
|
|
XCTAssertEqual(configured.dreamWeaverAuthenticationDescription, "API key configured")
|
|
|
|
let open = AppSessionConfiguration(
|
|
baseURL: "https://velocity.desineuron.in/api",
|
|
dreamWeaverBaseURL: "https://velocity.desineuron.in",
|
|
usesDedicatedDreamWeaverBaseURL: false,
|
|
hasDreamWeaverAPIKey: false,
|
|
email: nil,
|
|
hasPassword: false,
|
|
hasBearerToken: true,
|
|
source: .buildConfiguration
|
|
)
|
|
XCTAssertEqual(open.dreamWeaverAuthenticationDescription, "No gateway key configured")
|
|
}
|
|
|
|
func testGenerationJobBuildsFallbackRoutesWhenGatewayReturnsMinimalContract() throws {
|
|
let job = try JSONDecoder().decode(
|
|
GenerationJob.self,
|
|
from: Data(#"{"job_id":"job-123","status":"processing"}"#.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(job.pollUrl, nil)
|
|
XCTAssertEqual(job.resultUrl, nil)
|
|
XCTAssertEqual(
|
|
try job.resolvedPollURL(baseURL: "https://dw.desineuron.in").absoluteString,
|
|
"https://dw.desineuron.in/dream-weaver/status/job-123"
|
|
)
|
|
XCTAssertEqual(
|
|
try job.resolvedResultURL(baseURL: "https://dw.desineuron.in").absoluteString,
|
|
"https://dw.desineuron.in/dream-weaver/result/job-123"
|
|
)
|
|
}
|
|
|
|
func testJobStatusResolvesRelativeResultURL() throws {
|
|
let status = try JSONDecoder().decode(
|
|
JobStatus.self,
|
|
from: Data(#"{"status":"done","ready":true,"result_url":"/dream-weaver/result/job-123"}"#.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
try status.resolvedResultURL(baseURL: "https://dw.desineuron.in", jobId: "job-123").absoluteString,
|
|
"https://dw.desineuron.in/dream-weaver/result/job-123"
|
|
)
|
|
}
|
|
|
|
func testOracleProductionScopeOnlyExposesLiveBackedModes() {
|
|
XCTAssertEqual(
|
|
OracleModeAvailability.productionVisibleModes,
|
|
[
|
|
.pipeline,
|
|
.deals,
|
|
.accountTimeline,
|
|
.calendarTasks,
|
|
]
|
|
)
|
|
XCTAssertEqual(
|
|
OracleModeAvailability.hiddenModesUntilBackendSupport,
|
|
[
|
|
.teamPerformance,
|
|
.leadMap,
|
|
]
|
|
)
|
|
}
|
|
|
|
func testOracleProductionScopeSanitizesUnsupportedSelections() {
|
|
XCTAssertEqual(
|
|
OracleModeAvailability.sanitizedProductionSelection(.teamPerformance),
|
|
.pipeline
|
|
)
|
|
XCTAssertEqual(
|
|
OracleModeAvailability.sanitizedProductionSelection(.leadMap),
|
|
.pipeline
|
|
)
|
|
XCTAssertEqual(
|
|
OracleModeAvailability.sanitizedProductionSelection(.calendarTasks),
|
|
.calendarTasks
|
|
)
|
|
XCTAssertEqual(
|
|
OracleModeAvailability.sanitizedProductionSelection(.deals),
|
|
.deals
|
|
)
|
|
}
|
|
|
|
func testInventoryProductionScopeHidesDollhouseWithoutAsset() {
|
|
XCTAssertEqual(
|
|
InventoryModeAvailability.productionVisibleModes(hasDollhouseAsset: false),
|
|
[
|
|
.sunseeker,
|
|
.dreamWeaver,
|
|
]
|
|
)
|
|
XCTAssertEqual(
|
|
InventoryModeAvailability.modeSummaryText(hasDollhouseAsset: false),
|
|
"Sunseeker · Dream Weaver"
|
|
)
|
|
}
|
|
|
|
func testCanonicalContactAdapterFiltersRowsWithoutLeadContextAndNormalizesScore() {
|
|
let summaries = VelocityLeadDTO.activeLeadSummaries(
|
|
from: [
|
|
VelocityCanonicalContactListItemDTO(
|
|
personId: "person-1",
|
|
fullName: "Amina Rahman",
|
|
primaryPhone: "+971500000001",
|
|
buyerType: "high_intent",
|
|
leadId: "lead-1",
|
|
leadStatus: "qualified",
|
|
budgetBand: "AED 12M",
|
|
urgency: "high",
|
|
primaryInterest: "Marina Penthouse",
|
|
intentScore: 0.94,
|
|
engagementScore: 0.91,
|
|
urgencyScore: 0.88,
|
|
interactionCount: 6,
|
|
pendingTasks: 2,
|
|
lastInteractionAt: "2026-04-22T10:00:00+00:00",
|
|
createdAt: "2026-04-21T10:00:00+00:00"
|
|
),
|
|
VelocityCanonicalContactListItemDTO(
|
|
personId: "person-2",
|
|
fullName: "Contact Without Lead",
|
|
primaryPhone: nil,
|
|
buyerType: nil,
|
|
leadId: nil,
|
|
leadStatus: nil,
|
|
budgetBand: nil,
|
|
urgency: nil,
|
|
primaryInterest: nil,
|
|
intentScore: 0.42,
|
|
engagementScore: 0.10,
|
|
urgencyScore: 0.20,
|
|
interactionCount: 0,
|
|
pendingTasks: 0,
|
|
lastInteractionAt: nil,
|
|
createdAt: "2026-04-21T10:00:00+00:00"
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertEqual(summaries.count, 1)
|
|
XCTAssertEqual(summaries[0].id, "lead-1")
|
|
XCTAssertEqual(summaries[0].personId, "person-1")
|
|
XCTAssertEqual(summaries[0].score, 94)
|
|
XCTAssertEqual(summaries[0].kanbanStatus, "Qualified")
|
|
XCTAssertEqual(summaries[0].qualification, "Whale")
|
|
XCTAssertEqual(summaries[0].pendingTaskCount, 2)
|
|
XCTAssertEqual(summaries[0].interactionCount, 6)
|
|
XCTAssertEqual(summaries[0].updatedAt, "2026-04-22T10:00:00+00:00")
|
|
}
|
|
|
|
func testClientDataContactDecodesCurrentQDFamilies() throws {
|
|
let payload = """
|
|
{
|
|
"person_id": "person-live",
|
|
"full_name": "Sanjay Chatterjee",
|
|
"primary_phone": "+91-88479-41519",
|
|
"buyer_type": "investor",
|
|
"lead_id": "lead-live",
|
|
"lead_status": "new",
|
|
"budget_band": "15-25 Cr",
|
|
"urgency": "6_months",
|
|
"primary_interest": "Eden Devprayag",
|
|
"interaction_count": 4,
|
|
"pending_tasks": 1,
|
|
"last_interaction_at": "2026-04-18T16:47:12.740450+00:00",
|
|
"qd_overview": {
|
|
"intent": {
|
|
"score_type": "intent",
|
|
"current_value": 0.73
|
|
},
|
|
"engagement": {
|
|
"score_type": "engagement",
|
|
"current_value": 0.68
|
|
},
|
|
"urgency": {
|
|
"score_type": "urgency",
|
|
"current_value": 0.91
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
let contact = try JSONDecoder().decode(
|
|
VelocityCanonicalContactListItemDTO.self,
|
|
from: Data(payload.utf8)
|
|
)
|
|
let snapshot = VelocityClient360DTO.minimal(from: contact)
|
|
|
|
XCTAssertEqual(contact.personId, "person-live")
|
|
XCTAssertEqual(contact.intentScore, 0.73)
|
|
XCTAssertEqual(contact.engagementScore, 0.68)
|
|
XCTAssertEqual(contact.urgencyScore, 0.91)
|
|
XCTAssertEqual(contact.displayIntentScore, 91)
|
|
XCTAssertEqual(snapshot.primaryQDScore?.scoreType, "overall")
|
|
XCTAssertEqual(snapshot.primaryQDScore?.displayScore, 91)
|
|
}
|
|
|
|
func testClientDataDetailAdaptsNestedProductionShape() throws {
|
|
let payload = """
|
|
{
|
|
"person": {
|
|
"person_id": "person-live",
|
|
"full_name": "Sanjay Chatterjee",
|
|
"primary_email": "sanjay@example.com",
|
|
"primary_phone": "+91-88479-41519",
|
|
"buyer_type": "investor",
|
|
"persona_labels": ["high intent"]
|
|
},
|
|
"lead": {
|
|
"lead_id": "lead-live",
|
|
"lead_status": "qualified",
|
|
"budget_band": "15-25 Cr",
|
|
"urgency": "6_months"
|
|
},
|
|
"opportunities": [
|
|
{
|
|
"opportunity_id": "opp-live",
|
|
"stage": "site_visit",
|
|
"value": 18000000,
|
|
"probability": 0.7,
|
|
"expected_close_date": "2026-05-01",
|
|
"next_action": "Schedule visit",
|
|
"notes": null,
|
|
"project_id": "project-1",
|
|
"unit_id": null,
|
|
"person_id": "person-live",
|
|
"client_name": "Sanjay Chatterjee",
|
|
"client_phone": "+91-88479-41519",
|
|
"project_name": "Eden Devprayag"
|
|
}
|
|
],
|
|
"property_interests": [
|
|
{
|
|
"interest_id": "interest-live",
|
|
"project_name": "Eden Devprayag",
|
|
"unit_preference": "Penthouse",
|
|
"configuration": "4 BHK",
|
|
"budget_min": 15000000,
|
|
"budget_max": 25000000,
|
|
"priority": 1
|
|
}
|
|
],
|
|
"recent_interactions": [
|
|
{
|
|
"interaction_id": "interaction-live",
|
|
"channel": "call",
|
|
"interaction_type": "follow_up",
|
|
"happened_at": "2026-04-18T16:47:12.740450+00:00",
|
|
"summary": "Asked for tower availability."
|
|
}
|
|
],
|
|
"tasks": [
|
|
{
|
|
"reminder_id": "task-live",
|
|
"reminder_type": "follow_up",
|
|
"title": "Share floor plan",
|
|
"notes": "Send updated inventory.",
|
|
"due_at": "2026-04-26T10:00:00+00:00",
|
|
"status": "pending",
|
|
"priority": "high",
|
|
"person_id": "person-live",
|
|
"client_name": "Sanjay Chatterjee",
|
|
"client_phone": "+91-88479-41519"
|
|
}
|
|
],
|
|
"qd_scores": [
|
|
{
|
|
"score_type": "intent",
|
|
"current_value": 0.73,
|
|
"computed_at": "2026-04-18T16:47:12.740450+00:00",
|
|
"reasoning": "Recent call showed buying intent."
|
|
},
|
|
{
|
|
"score_type": "urgency",
|
|
"current_value": 0.91
|
|
}
|
|
],
|
|
"next_best_action": "Share inventory shortlist"
|
|
}
|
|
"""
|
|
|
|
let detail = try JSONDecoder().decode(
|
|
VelocityClientDataDetailDTO.self,
|
|
from: Data(payload.utf8)
|
|
).snapshot
|
|
|
|
XCTAssertEqual(detail.identity.personId, "person-live")
|
|
XCTAssertEqual(detail.identity.fullName, "Sanjay Chatterjee")
|
|
XCTAssertEqual(detail.currentLead?.leadId, "lead-live")
|
|
XCTAssertEqual(detail.currentLead?.status, "qualified")
|
|
XCTAssertEqual(detail.activeOpportunities.count, 1)
|
|
XCTAssertEqual(detail.propertyInterests.count, 1)
|
|
XCTAssertEqual(detail.recentInteractions.count, 1)
|
|
XCTAssertEqual(detail.tasks.count, 1)
|
|
XCTAssertEqual(detail.primaryQDScore?.scoreType, "intent")
|
|
XCTAssertEqual(detail.recommendedNextActions, ["Share inventory shortlist"])
|
|
}
|
|
|
|
func testCanonicalTaskAdapterSortsUrgentAndSoonerItemsFirst() {
|
|
let tasks = VelocityTaskDTO.sortedForOperatorReview(
|
|
[
|
|
VelocityTaskDTO(
|
|
reminderId: "task-3",
|
|
reminderType: "follow_up",
|
|
title: "Later high-priority visit",
|
|
notes: nil,
|
|
dueAt: "2026-04-24T10:00:00+00:00",
|
|
status: "pending",
|
|
priority: "high",
|
|
personId: "person-1",
|
|
clientName: "Amina Rahman",
|
|
clientPhone: "+971500000001"
|
|
),
|
|
VelocityTaskDTO(
|
|
reminderId: "task-1",
|
|
reminderType: "follow_up",
|
|
title: "Urgent callback",
|
|
notes: "Call now",
|
|
dueAt: "2026-04-23T08:00:00+00:00",
|
|
status: "pending",
|
|
priority: "urgent",
|
|
personId: "person-1",
|
|
clientName: "Amina Rahman",
|
|
clientPhone: "+971500000001"
|
|
),
|
|
VelocityTaskDTO(
|
|
reminderId: "task-2",
|
|
reminderType: "follow_up",
|
|
title: "Soon high-priority visit",
|
|
notes: nil,
|
|
dueAt: "2026-04-23T09:00:00+00:00",
|
|
status: "pending",
|
|
priority: "high",
|
|
personId: "person-1",
|
|
clientName: "Amina Rahman",
|
|
clientPhone: "+971500000001"
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertEqual(tasks.map(\.reminderId), ["task-1", "task-2", "task-3"])
|
|
XCTAssertEqual(tasks[0].priorityLabel, "Urgent")
|
|
XCTAssertNotNil(tasks[1].dueDate)
|
|
XCTAssertEqual(tasks[1].title, "Soon high-priority visit")
|
|
}
|
|
|
|
func testCanonicalTaskSnoozeUsesLaterOfNowOrDueDate() {
|
|
let task = VelocityTaskDTO(
|
|
reminderId: "task-1",
|
|
reminderType: "follow_up",
|
|
title: "Urgent callback",
|
|
notes: nil,
|
|
dueAt: "2099-04-23T08:00:00+00:00",
|
|
status: "pending",
|
|
priority: "urgent",
|
|
personId: "person-1",
|
|
clientName: "Amina Rahman",
|
|
clientPhone: "+971500000001"
|
|
)
|
|
|
|
let snoozed = task.nextSnoozeDate(adding: 2 * 60 * 60)
|
|
|
|
XCTAssertEqual(
|
|
Int(snoozed.timeIntervalSince(task.dueDate ?? .distantPast)),
|
|
2 * 60 * 60
|
|
)
|
|
}
|
|
|
|
func testCanonicalKanbanAdapterSortsCardsByIntentScoreWithinLane() {
|
|
let board = VelocityKanbanColumnDTO.operatorDisplayBoard(
|
|
from: [
|
|
VelocityKanbanColumnDTO(
|
|
status: "qualified",
|
|
label: "Qualified",
|
|
count: 2,
|
|
items: [
|
|
VelocityKanbanCardDTO(
|
|
leadId: "lead-2",
|
|
personId: "person-2",
|
|
clientName: "Zara Khan",
|
|
clientPhone: "+971500000002",
|
|
buyerType: "investor",
|
|
budgetBand: "AED 9M",
|
|
urgency: "high",
|
|
intentScore: 0.61
|
|
),
|
|
VelocityKanbanCardDTO(
|
|
leadId: "lead-1",
|
|
personId: "person-1",
|
|
clientName: "Amina Rahman",
|
|
clientPhone: "+971500000001",
|
|
buyerType: "high_intent",
|
|
budgetBand: "AED 12M",
|
|
urgency: "urgent",
|
|
intentScore: 0.94
|
|
),
|
|
]
|
|
),
|
|
]
|
|
)
|
|
|
|
XCTAssertEqual(board.count, 1)
|
|
XCTAssertEqual(board[0].items.map(\.leadId), ["lead-1", "lead-2"])
|
|
XCTAssertEqual(board[0].items[0].displayIntentScore, 94)
|
|
XCTAssertEqual(board[0].items[0].buyerTypeLabel, "High Intent")
|
|
XCTAssertEqual(board[0].items[0].urgencyLabel, "Urgent")
|
|
}
|
|
|
|
func testClient360ResponseDecodesCanonicalSnapshotShape() throws {
|
|
let payload = """
|
|
{
|
|
"client_ref": "person-1",
|
|
"snapshot_type": "client_360",
|
|
"identity": {
|
|
"person_id": "person-1",
|
|
"full_name": "Amina Rahman",
|
|
"primary_email": "amina@example.com",
|
|
"primary_phone": "+971500000001",
|
|
"buyer_type": "high_intent",
|
|
"persona_labels": ["marina_buyer"],
|
|
"source_confidence": 1.0
|
|
},
|
|
"current_lead": {
|
|
"lead_id": "lead-1",
|
|
"status": "qualified",
|
|
"budget_band": "AED 12M",
|
|
"urgency": "high",
|
|
"financing_posture": null,
|
|
"timeline_to_decision": null,
|
|
"objections": [],
|
|
"motivations": ["view_upgrade"]
|
|
},
|
|
"active_opportunities": [
|
|
{
|
|
"opportunity_id": "opp-1",
|
|
"stage": "proposal",
|
|
"value": 12000000,
|
|
"probability": 0.8,
|
|
"expected_close_date": "2026-04-30",
|
|
"next_action": "Share proposal",
|
|
"project_id": null,
|
|
"unit_id": null
|
|
}
|
|
],
|
|
"recent_interactions": [
|
|
{
|
|
"interaction_id": "int-1",
|
|
"channel": "whatsapp",
|
|
"interaction_type": "message",
|
|
"happened_at": "2026-04-22T10:00:00+00:00",
|
|
"summary": "Asked for brochure"
|
|
}
|
|
],
|
|
"property_interests": [
|
|
{
|
|
"interest_id": "interest-1",
|
|
"project_name": "Marina Residences",
|
|
"unit_preference": "4BR",
|
|
"configuration": "Penthouse",
|
|
"budget_min": 10000000,
|
|
"budget_max": 15000000,
|
|
"priority": 1
|
|
}
|
|
],
|
|
"tasks": [
|
|
{
|
|
"reminder_id": "task-1",
|
|
"reminder_type": "follow_up",
|
|
"title": "Call back",
|
|
"due_at": "2026-04-23T09:00:00+00:00",
|
|
"status": "pending",
|
|
"priority": "high"
|
|
}
|
|
],
|
|
"qd_overview": {
|
|
"intent_score": {
|
|
"score_type": "intent_score",
|
|
"current_value": 0.94,
|
|
"computed_at": "2026-04-22T10:00:00+00:00",
|
|
"reasoning": "Strong engagement"
|
|
}
|
|
},
|
|
"risk_flags": ["no_recent_interactions"],
|
|
"recommended_next_actions": ["Schedule follow-up"],
|
|
"note": "Derived read model. Not primary truth."
|
|
}
|
|
"""
|
|
|
|
let snapshot = try JSONDecoder().decode(
|
|
VelocityClient360DTO.self,
|
|
from: Data(payload.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(snapshot.identity.fullName, "Amina Rahman")
|
|
XCTAssertEqual(snapshot.currentLead?.status, "qualified")
|
|
XCTAssertEqual(snapshot.activeOpportunities.first?.formattedValue, "AED 12.0M")
|
|
XCTAssertEqual(snapshot.qdOverview["intent_score"]?.displayScore, 94)
|
|
XCTAssertEqual(snapshot.tasks.first?.title, "Call back")
|
|
}
|
|
|
|
func testClient360DecodesLiveJsonStringArrayFields() throws {
|
|
let payload = """
|
|
{
|
|
"client_ref": "person-live",
|
|
"snapshot_type": "client_360",
|
|
"identity": {
|
|
"person_id": "person-live",
|
|
"full_name": "Sanjay Chatterjee",
|
|
"primary_email": "sanjay@example.com",
|
|
"primary_phone": "+91-88479-41519",
|
|
"buyer_type": null,
|
|
"persona_labels": "[\\"repeat_visitor\\"]",
|
|
"source_confidence": 0.94
|
|
},
|
|
"current_lead": {
|
|
"lead_id": "lead-live",
|
|
"status": "new",
|
|
"budget_band": "15-25 Cr",
|
|
"urgency": "6_months",
|
|
"financing_posture": null,
|
|
"timeline_to_decision": null,
|
|
"objections": "[]",
|
|
"motivations": "[\\"family_discussion\\"]"
|
|
},
|
|
"active_opportunities": [],
|
|
"recent_interactions": [],
|
|
"property_interests": [],
|
|
"tasks": [],
|
|
"qd_overview": {
|
|
"urgency": {
|
|
"score_type": "urgency",
|
|
"current_value": 1.0,
|
|
"computed_at": "2026-04-18T16:47:12.740450+00:00",
|
|
"reasoning": null
|
|
}
|
|
},
|
|
"risk_flags": ["no_property_interests_recorded"],
|
|
"recommended_next_actions": [],
|
|
"note": "Derived read model. Not primary truth."
|
|
}
|
|
"""
|
|
|
|
let snapshot = try JSONDecoder().decode(
|
|
VelocityClient360DTO.self,
|
|
from: Data(payload.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(snapshot.identity.personaLabels, ["repeat_visitor"])
|
|
XCTAssertEqual(snapshot.currentLead?.objections, [])
|
|
XCTAssertEqual(snapshot.currentLead?.motivations, ["family_discussion"])
|
|
XCTAssertEqual(snapshot.qdOverview["urgency"]?.displayScore, 100)
|
|
}
|
|
|
|
func testOpportunitiesClientSortsHigherValueRowsFirst() {
|
|
let opportunities = [
|
|
VelocityOpportunityDTO(
|
|
opportunityId: "opp-2",
|
|
stage: "proposal",
|
|
value: 8000000,
|
|
probability: 0.6,
|
|
expectedCloseDate: nil,
|
|
nextAction: "Send brochure",
|
|
notes: nil,
|
|
projectId: nil,
|
|
unitId: nil,
|
|
personId: "person-2",
|
|
clientName: "Zara Khan",
|
|
clientPhone: "+971500000002",
|
|
projectName: "Skyline"
|
|
),
|
|
VelocityOpportunityDTO(
|
|
opportunityId: "opp-1",
|
|
stage: "negotiation",
|
|
value: 12000000,
|
|
probability: 0.8,
|
|
expectedCloseDate: nil,
|
|
nextAction: "Share proposal",
|
|
notes: nil,
|
|
projectId: nil,
|
|
unitId: nil,
|
|
personId: "person-1",
|
|
clientName: "Amina Rahman",
|
|
clientPhone: "+971500000001",
|
|
projectName: "Marina Residences"
|
|
),
|
|
].sorted { lhs, rhs in
|
|
let leftValue = lhs.value ?? 0
|
|
let rightValue = rhs.value ?? 0
|
|
if leftValue != rightValue {
|
|
return leftValue > rightValue
|
|
}
|
|
return (lhs.clientName ?? "").localizedCaseInsensitiveCompare(rhs.clientName ?? "") == .orderedAscending
|
|
}
|
|
|
|
XCTAssertEqual(opportunities.map(\.opportunityId), ["opp-1", "opp-2"])
|
|
XCTAssertEqual(opportunities[0].formattedValue, "AED 12.0M")
|
|
XCTAssertEqual(opportunities[0].probabilityLabel, "80% probability")
|
|
}
|
|
|
|
func testOpportunityMutationResponseDecodesCanonicalShape() throws {
|
|
let payload = """
|
|
{
|
|
"opportunity_id": "opp-1",
|
|
"stage": "negotiation",
|
|
"value": 12000000,
|
|
"probability": 75,
|
|
"expected_close_date": "2026-04-30",
|
|
"next_action": "Schedule commercial review",
|
|
"notes": "Updated from iPad",
|
|
"person_id": "person-1",
|
|
"client_name": "Amina Rahman",
|
|
"client_phone": "+971500000001",
|
|
"project_name": "Marina Residences"
|
|
}
|
|
"""
|
|
|
|
let opportunity = try JSONDecoder().decode(
|
|
VelocityOpportunityDTO.self,
|
|
from: Data(payload.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(opportunity.opportunityId, "opp-1")
|
|
XCTAssertEqual(opportunity.stage, "negotiation")
|
|
XCTAssertEqual(opportunity.probabilityPercent, 75)
|
|
XCTAssertEqual(opportunity.probabilityLabel, "75% probability")
|
|
XCTAssertEqual(opportunity.nextAction, "Schedule commercial review")
|
|
XCTAssertEqual(opportunity.notes, "Updated from iPad")
|
|
}
|
|
|
|
func testLeadStageMutationResponseDecodesCanonicalShape() throws {
|
|
let payload = """
|
|
{
|
|
"lead_id": "lead-1",
|
|
"person_id": "person-1",
|
|
"status": "qualified",
|
|
"budget_band": "AED 12M",
|
|
"urgency": "high",
|
|
"client_name": "Amina Rahman",
|
|
"client_phone": "+971500000001"
|
|
}
|
|
"""
|
|
|
|
let update = try JSONDecoder().decode(
|
|
VelocityLeadStageUpdateDTO.self,
|
|
from: Data(payload.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(update.leadId, "lead-1")
|
|
XCTAssertEqual(update.personId, "person-1")
|
|
XCTAssertEqual(update.status, "qualified")
|
|
XCTAssertEqual(update.clientName, "Amina Rahman")
|
|
}
|
|
|
|
func testImportBatchDetailDecodesCanonicalReviewShape() throws {
|
|
let payload = """
|
|
{
|
|
"batch_id": "batch-1",
|
|
"source_system": "csv_upload",
|
|
"filename": "clients.csv",
|
|
"row_count": 1,
|
|
"mapping_manifest": {"mapped_count": 4},
|
|
"lifecycle": "parsed",
|
|
"proposals": [
|
|
{
|
|
"proposal_id": "proposal-1",
|
|
"payload": {
|
|
"row_number": 1,
|
|
"canonical_payload": {"full_name": "Amina Rahman", "primary_phone": "+971500000001"},
|
|
"raw_row": {"Name": "Amina Rahman"},
|
|
"unresolved_fields": [],
|
|
"missing_required": [],
|
|
"confidence": 0.92,
|
|
"review_required": true
|
|
},
|
|
"confidence": 0.92,
|
|
"status": "pending",
|
|
"review_required": true
|
|
}
|
|
],
|
|
"proposal_count": 1
|
|
}
|
|
"""
|
|
|
|
let detail = try JSONDecoder().decode(
|
|
VelocityImportBatchDetailDTO.self,
|
|
from: Data(payload.utf8)
|
|
)
|
|
|
|
XCTAssertEqual(detail.batchId, "batch-1")
|
|
XCTAssertEqual(detail.proposals.count, 1)
|
|
XCTAssertEqual(detail.proposals[0].confidencePercent, 92)
|
|
XCTAssertEqual(detail.proposals[0].payload?.canonicalPayload?["full_name"]?.stringValue, "Amina Rahman")
|
|
}
|
|
|
|
func testInventoryProductionScopeShowsDollhouseWhenAssetIsShipped() {
|
|
XCTAssertEqual(
|
|
InventoryModeAvailability.productionVisibleModes(hasDollhouseAsset: true),
|
|
[
|
|
.sunseeker,
|
|
.dreamWeaver,
|
|
.dollhouse,
|
|
]
|
|
)
|
|
XCTAssertEqual(
|
|
InventoryModeAvailability.modeSummaryText(hasDollhouseAsset: true),
|
|
"Sunseeker · Dream Weaver · Dollhouse"
|
|
)
|
|
}
|
|
|
|
func testInventoryProductionScopeSanitizesUnsupportedSelection() {
|
|
XCTAssertEqual(
|
|
InventoryModeAvailability.sanitizedProductionSelection(.dollhouse, hasDollhouseAsset: false),
|
|
.sunseeker
|
|
)
|
|
XCTAssertEqual(
|
|
InventoryModeAvailability.sanitizedProductionSelection(.dollhouse, hasDollhouseAsset: true),
|
|
.dollhouse
|
|
)
|
|
}
|
|
|
|
func testSentinelScopeReflectsOperatorPosturePositioning() {
|
|
XCTAssertEqual(SentinelScope.navigationTitle, "Operator Posture")
|
|
XCTAssertEqual(SentinelScope.productFamilyName, "Sentinel")
|
|
XCTAssertEqual(SentinelScope.availabilityBadge, "Operator posture only")
|
|
XCTAssertEqual(
|
|
SentinelScope.disabledAnalyticsSummary,
|
|
"visitor counting, facial detections, sentiment scoring"
|
|
)
|
|
XCTAssertEqual(
|
|
SentinelScope.liveBackedSummary,
|
|
"alert posture, transcription queue visibility, upcoming calendar pressure, recent operator timeline"
|
|
)
|
|
}
|
|
|
|
func testAppStoreRefreshPolicyMatchesWebOSInventorySlice() {
|
|
XCTAssertEqual(AppStoreRefreshPolicy.inventoryPropertyLimit, 100)
|
|
XCTAssertEqual(AppStoreRefreshPolicy.canonicalTaskLimit, 50)
|
|
XCTAssertEqual(AppStoreRefreshPolicy.leadTimelineHydrationLimit, 6)
|
|
XCTAssertEqual(AppStoreRefreshPolicy.leadEventLimitPerLead, 4)
|
|
}
|
|
|
|
func testAppStoreRefreshPolicyPrioritizesHighestScoreLeads() {
|
|
let leads = [
|
|
VelocityLeadDTO(
|
|
id: "lead-1",
|
|
personId: "person-1",
|
|
name: "Lead One",
|
|
phone: nil,
|
|
source: "website",
|
|
qualification: "POTENTIAL",
|
|
score: 40,
|
|
kanbanStatus: "New",
|
|
budget: "",
|
|
unitInterest: "",
|
|
pendingTaskCount: 0,
|
|
interactionCount: 0,
|
|
createdAt: nil,
|
|
updatedAt: nil
|
|
),
|
|
VelocityLeadDTO(
|
|
id: "lead-2",
|
|
personId: "person-2",
|
|
name: "Lead Two",
|
|
phone: nil,
|
|
source: "website",
|
|
qualification: "HOT",
|
|
score: 95,
|
|
kanbanStatus: "Negotiation",
|
|
budget: "",
|
|
unitInterest: "",
|
|
pendingTaskCount: 1,
|
|
interactionCount: 3,
|
|
createdAt: nil,
|
|
updatedAt: nil
|
|
),
|
|
VelocityLeadDTO(
|
|
id: "lead-3",
|
|
personId: "person-3",
|
|
name: "Lead Three",
|
|
phone: nil,
|
|
source: "walkin",
|
|
qualification: "WHALE",
|
|
score: 88,
|
|
kanbanStatus: "Qualified",
|
|
budget: "",
|
|
unitInterest: "",
|
|
pendingTaskCount: 0,
|
|
interactionCount: 1,
|
|
createdAt: nil,
|
|
updatedAt: nil
|
|
),
|
|
]
|
|
|
|
XCTAssertEqual(
|
|
AppStoreRefreshPolicy.prioritizedLeadIDs(from: leads, limit: 2),
|
|
["lead-2", "lead-3"]
|
|
)
|
|
}
|
|
}
|