Merge Conflicts (#41)
Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #41
This commit was merged in pull request #41.
This commit is contained in:
973
iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift
Normal file
973
iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift
Normal file
@@ -0,0 +1,973 @@
|
||||
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"]
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user