| 1 | import assert from "node:assert/strict"; |
| 2 | import { readFileSync } from "node:fs"; |
| 3 | import test from "node:test"; |
| 4 | |
| 5 | const workspace = readFileSync(new URL("../app/components/investment-workspace.tsx", import.meta.url), "utf8"); |
| 6 | const api = readFileSync(new URL("../app/lib/portfolio-api.ts", import.meta.url), "utf8"); |
| 7 | const styles = readFileSync(new URL("../app/styles.css", import.meta.url), "utf8"); |
| 8 | |
| 9 | test("renders persisted multi-portfolio totals while keeping holdings out of the dashboard", () => { |
| 10 | assert.match(workspace, />All<\/button>/); |
| 11 | assert.match(workspace, /Object\.entries\(dashboard\.currencyTotals\)/); |
| 12 | assert.match(workspace, /dashboard\.portfolios\.map/); |
| 13 | assert.doesNotMatch(workspace, /dashboard\.combinedHoldings\.map/); |
| 14 | }); |
| 15 | |
| 16 | test("manual-import display-name editing lives inside the holding drawer", () => { |
| 17 | assert.match(workspace, /position\.sourceType === "MANUAL_CSV_IMPORT"/); |
| 18 | assert.match(workspace, />Edit<\/button>/); |
| 19 | assert.match(workspace, />Save<\/Button>/); |
| 20 | assert.match(workspace, />Cancel<\/Button>/); |
| 21 | assert.doesNotMatch(workspace, />Edit name<\/button>/); |
| 22 | assert.match(workspace, /maxLength=\{160\}/); |
| 23 | assert.match(api, /positions\/\$\{positionId\}\/display-name/); |
| 24 | assert.doesNotMatch(workspace, /dangerouslySetInnerHTML/); |
| 25 | assert.match(workspace, /<strong>\{position\.displayName\}<\/strong>/); |
| 26 | }); |
| 27 | |
| 28 | test("research selection uses global instrument identity rather than a display ticker alias", () => { |
| 29 | assert.match(api, /globalInstrumentId\?: string \| null/); |
| 30 | assert.match(workspace, /company\.instrumentId === globalInstrumentId/); |
| 31 | assert.match(workspace, /value: globalInstrumentId \?\? resolved\?\.instrumentId \?\? instrument\.instrumentId/); |
| 32 | assert.match(workspace, /globalInstrumentId,/); |
| 33 | assert.match(workspace, /canRefreshResearch\(company\)/); |
| 34 | assert.doesNotMatch(workspace, /KPITE\s*[-=]>\s*KPITTECH/); |
| 35 | }); |
| 36 | |
| 37 | test("refresh eligibility is based on canonical global identity, not a portfolio-summary row or existing research", () => { |
| 38 | const eligibility = workspace.match(/function canRefreshResearch[\s\S]*?\n}\n\nfunction researchEmptyTitle/)?.[0] ?? ""; |
| 39 | assert.match(eligibility, /Boolean\(company\?\.instrumentId\)/); |
| 40 | assert.doesNotMatch(eligibility, /documentCount|eventCount|confidence|freshness|lastRefresh/); |
| 41 | assert.match(eligibility, /"COMPANY_NOT_RESOLVED"/); |
| 42 | assert.match(eligibility, /"RESEARCH_NOT_APPLICABLE"/); |
| 43 | assert.match(eligibility, /"ETF_UNSUPPORTED"/); |
| 44 | assert.match(workspace, /function canRefreshResearchIdentity\([\s\S]*?Boolean\(globalInstrumentId\?\.trim\(\)\)/); |
| 45 | assert.match(workspace, /selectedContextResearchCompany\?\.status\s*\?\? \(selectedContextResearchCompany \? undefined : selectedResearchOption\?\.status\)/); |
| 46 | assert.match(workspace, /disabled=\{loading \|\| !refreshEligible\}/); |
| 47 | assert.match(workspace, /selectedResearchInstrumentId=\{selectedResearchInstrumentId\}/); |
| 48 | assert.match(workspace, /openResearchReadiness\([\s\S]*?selectedResearchInstrumentId/); |
| 49 | }); |
| 50 | |
| 51 | test("Talbros/Ujjivan-shaped global selections are refreshable while unresolved and unsupported selections are not", () => { |
| 52 | const eligibility = workspace.match(/function canRefreshResearchIdentity[\s\S]*?\n}\n\nfunction researchEmptyTitle/)?.[0] ?? ""; |
| 53 | // The helper deliberately ignores documents/events/confidence/freshness and |
| 54 | // gates only canonical identity, active loading, explicit status, and asset type. |
| 55 | assert.doesNotMatch(eligibility, /documentCount|eventCount|confidence|freshness|lastRefresh/); |
| 56 | assert.match(workspace, /globalInstrumentId \? "GLOBAL_INSTRUMENT_RESOLVED" : "COMPANY_NOT_RESOLVED"/); |
| 57 | assert.match(eligibility, /"COMPANY_NOT_RESOLVED"/); |
| 58 | assert.match(eligibility, /"ETF_UNSUPPORTED"/); |
| 59 | assert.match(eligibility, /\["ETF", "FUND", "BOND", "CASH", "CRYPTO"\]/); |
| 60 | assert.match(workspace, /value: globalInstrumentId \?\? resolved\?\.instrumentId/); |
| 61 | assert.match(workspace, /openResearchReadiness\([\s\S]*?selectedResearchInstrumentId/); |
| 62 | }); |
| 63 | |
| 64 | test("portfolio detail and research-table drawers join global research to a different local holding ID", () => { |
| 65 | const holdingsView = workspace.match(/function HoldingsTable\([\s\S]*?\n}\n\nfunction metricText/)?.[0] ?? ""; |
| 66 | assert.match(holdingsView, /company\.instrumentId === position\.instrument\.globalInstrumentId/); |
| 67 | assert.match(holdingsView, /company\.instrumentId === position\.instrument\.instrumentId/); |
| 68 | assert.match(holdingsView, /<StockResearchDrawer position=\{detail\.position\} research=\{detail\.researchInstrumentId \? portfolioResearch\?\.companies\.find/); |
| 69 | assert.match(workspace, /position\.instrument\.globalInstrumentId === company\.instrumentId/); |
| 70 | assert.match(workspace, /market\?\.resolution\.providerTicker/); |
| 71 | assert.match(workspace, /research\?\.structuredMarket\?\.facts\[key\]/); |
| 72 | assert.match(workspace, /latestQuarterlyResult|shareholdingChanges|currentQuarterCatalysts|sourceDiversity/); |
| 73 | }); |
| 74 | |
| 75 | test("renders public broker cards and hides the demo provider in normal mode", () => { |
| 76 | assert.match(workspace, /visibleProviders\.map/); |
| 77 | assert.match(workspace, /provider\.brokerType !== "MOCK"/); |
| 78 | assert.match(workspace, /provider\.connectable/); |
| 79 | assert.match(workspace, /provider\.unavailableReason \?\? "Connection not available yet\."/); |
| 80 | assert.match(api, /\/api\/v1\/brokers/); |
| 81 | }); |
| 82 | |
| 83 | test("uses generic sync and authentication continuation contracts", () => { |
| 84 | assert.match(api, /portfolios\/broker-connections\/\$\{connectionId\}\/sync/); |
| 85 | assert.match(workspace, /refreshAuthenticatedBroker/); |
| 86 | assert.match(workspace, /REDIRECT_REQUIRED/); |
| 87 | }); |
| 88 | |
| 89 | test("consumer broker flow never asks for retail API credentials", () => { |
| 90 | assert.match(api, /PARTNER_AUTH_UNAVAILABLE/); |
| 91 | assert.doesNotMatch(workspace, /USER_CREDENTIALS_REQUIRED|broker password|OTP|MFA/); |
| 92 | assert.match(workspace, /provider\.individualApiSupported && provider\.advancedIndividualMode/); |
| 93 | assert.match(workspace, /activeConnection\.brokerType === "IBKR" \? "Connect \/ Re-authenticate" : "Connect"/); |
| 94 | }); |
| 95 | |
| 96 | test("opens a generic broker authentication popup synchronously before API work", () => { |
| 97 | assert.match(workspace, /window\.open\("about:blank", "aip-ibkr-auth", features\)/); |
| 98 | assert.match(workspace, /popup=yes,width=\$\{width\},height=\$\{height\},left=\$\{left\},top=\$\{top\},resizable=yes,scrollbars=yes/); |
| 99 | assert.match(workspace, /const authWindow = openBrokerAuthenticationWindow\(provider\.brokerType\);[\s\S]*?await brokerApi\.connectBroker/); |
| 100 | assert.match(workspace, /authWindow\.location\.assign\(action\.authenticationUrl\)/); |
| 101 | assert.match(workspace, /action\.action === "REDIRECT_REQUIRED" \|\| action\.action === "POPUP_REQUIRED"/); |
| 102 | }); |
| 103 | |
| 104 | test("popup flow handles blocked windows, NONE, failures, and missing URLs safely", () => { |
| 105 | assert.match(workspace, /Your browser blocked the IBKR sign-in window\. Allow pop-ups for this site and try again\./); |
| 106 | assert.match(workspace, /if \(action\.action === "NONE"\)/); |
| 107 | assert.match(workspace, /if \(!action\.authenticationUrl\)/); |
| 108 | assert.match(workspace, /authWindow\.close\(\)/); |
| 109 | assert.match(workspace, /Your saved portfolio remains available/); |
| 110 | }); |
| 111 | |
| 112 | test("popup handling is provider-neutral and unsupported providers never receive a URL", () => { |
| 113 | assert.doesNotMatch(workspace, /authenticationUrl.*IBKR|authenticationUrl.*ICICI|authenticationUrl.*HDFC/); |
| 114 | assert.match(api, /"UNAVAILABLE" \| "UNSUPPORTED"/); |
| 115 | }); |
| 116 | |
| 117 | test("keeps market-price and broker-sync freshness visibly separate", () => { |
| 118 | assert.match(workspace, /Broker holdings synced:/); |
| 119 | assert.match(workspace, /Market price updated/); |
| 120 | }); |
| 121 | |
| 122 | test("authentication-required IBKR uses re-authentication and broker cards do not offer Sync", () => { |
| 123 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 124 | assert.match(brokerView, /Connect \/ Re-authenticate/); |
| 125 | assert.doesNotMatch(brokerView, />Sync</); |
| 126 | assert.match(brokerView, /Scalable broker authentication|unavailableReason/); |
| 127 | }); |
| 128 | |
| 129 | test("connected IBKR maps to Manage and Disconnect", () => { |
| 130 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 131 | assert.match(brokerView, /activeConnection && connected \? <Button variant="secondary" disabled>Manage<\/Button>/); |
| 132 | assert.match(brokerView, />Disconnect<\/Button>/); |
| 133 | }); |
| 134 | |
| 135 | test("disconnected and error IBKR remain recoverable instead of Disconnect-only", () => { |
| 136 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 137 | assert.match(brokerView, /const recoverable = Boolean\(activeConnection\) && !connected/); |
| 138 | assert.match(brokerView, /activeConnection && recoverable/); |
| 139 | assert.match(brokerView, /Connect \/ Re-authenticate/); |
| 140 | }); |
| 141 | |
| 142 | test("authentication-required IBKR exposes the reconnect action", () => { |
| 143 | assert.match(workspace, /authenticationRequired[\s\S]*?recoverable/); |
| 144 | assert.match(workspace, /activeConnection\.brokerType === "IBKR" \? "Connect \/ Re-authenticate" : "Connect"/); |
| 145 | }); |
| 146 | |
| 147 | test("HDFC and ICICI partner-unavailable Connect does not open a popup", () => { |
| 148 | assert.match(workspace, /provider\.consumerAuthMode === "PARTNER_UNAVAILABLE"[\s\S]*?Direct customer account connection is not available yet\.[\s\S]*?return;[\s\S]*?openBrokerAuthenticationWindow/); |
| 149 | assert.match(api, /consumerAuthMode: "BROKER_REDIRECT" \| "PARTNER_OAUTH" \| "INDIVIDUAL_API_CREDENTIALS" \| "PARTNER_UNAVAILABLE" \| "NONE"/); |
| 150 | }); |
| 151 | |
| 152 | test("partner-unavailable and authentication errors are broker-card-local", () => { |
| 153 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 154 | assert.match(brokerView, /errors\[provider\.brokerType\]/); |
| 155 | assert.doesNotMatch(brokerView, /\{error \? <div className="broker-inline-error"/); |
| 156 | }); |
| 157 | |
| 158 | test("broker authentication failure preserves the Brokers page", () => { |
| 159 | assert.match(workspace, /setBrokerCardError\(brokerType, getApiFailure\(err\)\.message\)/); |
| 160 | assert.match(workspace, /visibleProviders\.map/); |
| 161 | assert.doesNotMatch(workspace, /setError\(getApiFailure\(err\)\)[\s\S]{0,120}completeBrokerAuthentication/); |
| 162 | }); |
| 163 | |
| 164 | test("normal broker rendering consumes the backend canonical connection list", () => { |
| 165 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 166 | assert.match(api, /listConnections: \(\) => request<BrokerConnection\[]>\("\/api\/v1\/broker-connections"\)/); |
| 167 | assert.match(brokerView, /connections\.find\(\(connection\) => connection\.brokerType === provider\.brokerType\)/); |
| 168 | assert.match(brokerView, /linkedPortfolio[\s\S]*?portfolio\.brokerConnectionId === activeConnection\.connectionId/); |
| 169 | }); |
| 170 | |
| 171 | test("partner unavailable has a dedicated badge and no meaningless disconnect", () => { |
| 172 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 173 | assert.match(brokerView, /partnerUnavailable \? "Direct connection unavailable"/); |
| 174 | assert.match(brokerView, /Direct customer account connection is not available yet\./); |
| 175 | assert.match(brokerView, /meaningfulPersistedLinkage/); |
| 176 | assert.match(brokerView, /!partnerUnavailable \|\| meaningfulPersistedLinkage/); |
| 177 | assert.match(brokerView, /!partnerUnavailable && !developerIndividualMode && !activeConnection && provider\.connectable/); |
| 178 | assert.match(brokerView, /!partnerUnavailable && !developerIndividualMode && activeConnection && recoverable/); |
| 179 | assert.match(brokerView, /!partnerUnavailable && errors\[provider\.brokerType\]/); |
| 180 | }); |
| 181 | |
| 182 | test("provider metadata drives partner, unavailable, and advanced individual modes", () => { |
| 183 | assert.match(api, /"PARTNER_OAUTH" \| "INDIVIDUAL_API_CREDENTIALS" \| "PARTNER_UNAVAILABLE"/); |
| 184 | assert.match(api, /individualApiSupported: boolean/); |
| 185 | assert.match(api, /advancedIndividualMode: boolean/); |
| 186 | assert.match(workspace, /developerIndividualMode \? \(/); |
| 187 | assert.match(workspace, /Configure DEV API/); |
| 188 | assert.match(workspace, /Credentials are stored write-only/); |
| 189 | assert.match(workspace, /setDeveloperSecret\(""\)/); |
| 190 | assert.doesNotMatch(workspace, /provider\.brokerType === "HDFC_SECURITIES".*developer|provider\.brokerType === "ICICI_DIRECT".*developer/); |
| 191 | }); |
| 192 | |
| 193 | test("IBKR redirect keeps its popup open and NONE closes it", () => { |
| 194 | assert.match(workspace, /action\.action === "REDIRECT_REQUIRED" \|\| action\.action === "POPUP_REQUIRED"/); |
| 195 | assert.match(workspace, /authWindow\.location\.assign\(action\.authenticationUrl\)[\s\S]*?return true/); |
| 196 | assert.match(workspace, /authWindow\.close\(\);[\s\S]*?if \(action\.action === "NONE"\)/); |
| 197 | }); |
| 198 | |
| 199 | test("saved IBKR portfolio remains rendered while authentication is required", () => { |
| 200 | const brokerView = workspace.match(/function BrokerView\([\s\S]*?\nfunction ResearchView/)?.[0] ?? ""; |
| 201 | assert.match(brokerView, /Authentication is required to refresh holdings\. Your saved portfolio remains available\./); |
| 202 | assert.match(brokerView, /linkedPortfolio \? <p className="broker-linked-portfolio">/); |
| 203 | }); |
| 204 | |
| 205 | test("Sync is rendered only when a linked broker portfolio supplies an action", () => { |
| 206 | assert.match(workspace, /selectedPortfolio\?\.brokerConnectionId \? syncSelectedPortfolio : undefined/); |
| 207 | assert.match(workspace, /\{onSync \? <Button/); |
| 208 | }); |
| 209 | |
| 210 | test("broker status badges and actions cannot wrap character by character", () => { |
| 211 | assert.match(styles, /\.broker-card \.badge,[\s\S]*?white-space: nowrap/); |
| 212 | assert.match(styles, /word-break: normal/); |
| 213 | }); |
| 214 | |
| 215 | test("persisted broker portfolios remain visible with unknown historical sync time", () => { |
| 216 | assert.match(workspace, /Imported previously; sync time unknown/); |
| 217 | assert.match(workspace, /Your saved portfolio remains available/); |
| 218 | }); |
| 219 | |
| 220 | test("does not expose connector or session internals in frontend connection types", () => { |
| 221 | const publicConnectionType = api.match(/export type BrokerConnection = \{[\s\S]*?\n\};/)?.[0] ?? ""; |
| 222 | assert.doesNotMatch(publicConnectionType, /connectorId/); |
| 223 | assert.doesNotMatch(publicConnectionType, /sessionReference/); |
| 224 | assert.doesNotMatch(publicConnectionType, /externalAccountReference/); |
| 225 | const descriptorType = api.match(/export type BrokerProviderInfo = \{[\s\S]*?\n\};/)?.[0] ?? ""; |
| 226 | assert.doesNotMatch(descriptorType, /authenticationModel|capabilities|connectionMethod/); |
| 227 | }); |
| 228 | |
| 229 | test("manual import uses the exact multipart preview contract and derives browser-suffixed accounts", () => { |
| 230 | assert.match(api, /body\.append\("file", file\)/); |
| 231 | assert.match(api, /init\?\.body instanceof FormData \? \{\} : \{ "Content-Type": "application\/json" \}/); |
| 232 | assert.doesNotMatch(api, /previewImport[\s\S]{0,500}"Content-Type": "multipart\/form-data"/); |
| 233 | assert.match(api, /imports\/\$\{brokerType\.toLowerCase\(\)\}\/preview/); |
| 234 | assert.match(workspace, /detectedImportAccount/); |
| 235 | assert.match(workspace, /PortFolioEqtSummary/); |
| 236 | assert.match(workspace, /Invest Right Equity Portfolio_/); |
| 237 | assert.match(workspace, /Preview portfolio/); |
| 238 | }); |
| 239 | |
| 240 | test("manual import is a polished dialog with statement prices and useful backend errors", () => { |
| 241 | assert.match(workspace, /role="dialog"/); |
| 242 | assert.match(workspace, /import-modal-backdrop/); |
| 243 | assert.match(workspace, /Statement price/); |
| 244 | assert.match(workspace, /getApiFailure\(error\)\.message/); |
| 245 | assert.match(styles, /\.import-dropzone/); |
| 246 | assert.match(workspace, /Portfolio imported successfully/); |
| 247 | }); |
| 248 | |
| 249 | test("manual portfolios expose explicit price refresh without a selection-triggered sweep", () => { |
| 250 | assert.match(api, /refreshPrices: \(portfolioId: string\)/); |
| 251 | assert.match(api, /\/prices\/refresh/); |
| 252 | assert.match(api, /importedPrice\?: Money \| null/); |
| 253 | assert.match(workspace, /acquisitionSource === "MANUAL_CSV_IMPORT" \? refreshSelectedPrices/); |
| 254 | assert.doesNotMatch(workspace, /automaticallyRefreshedPricesRef/); |
| 255 | assert.doesNotMatch(workspace, /void refreshSelectedPrices\(\)/); |
| 256 | assert.match(workspace, /Refresh Prices/); |
| 257 | assert.match(workspace, /Latest Price/); |
| 258 | assert.match(workspace, /Imported Price/); |
| 259 | assert.match(workspace, /Provider/); |
| 260 | assert.match(workspace, /Market As Of/); |
| 261 | assert.match(workspace, /Retrieved At/); |
| 262 | }); |
| 263 | |
| 264 | test("IBKR authentication completion message resumes the canonical connection", () => { |
| 265 | assert.match(workspace, /aip:ibkr-authenticated/); |
| 266 | assert.match(workspace, /event\.origin !== window\.location\.origin/); |
| 267 | assert.match(workspace, /finishBrokerAuthentication\(pending\.connectionId\)/); |
| 268 | }); |
| 269 | |
| 270 | test("IBKR popup lifecycle retains one popup and polls the canonical connection", () => { |
| 271 | assert.match(workspace, /brokerAuthPopupRef = useRef<Window \| null>/); |
| 272 | assert.match(workspace, /brokerAuthPopupRef\.current\.focus\(\)/); |
| 273 | assert.match(workspace, /pendingBrokerAuthRef\.current = \{ connectionId, provider \}/); |
| 274 | assert.match(workspace, /window\.setInterval\(\(\) => void poll\(\), brokerAuthenticationPollMs\)/); |
| 275 | assert.match(workspace, /brokerApi\.getAuthStatus\(connectionId\)/); |
| 276 | assert.match(workspace, /if \(!authStatus\.authenticated \|\| authStatus\.state !== "CONNECTED"\) return false/); |
| 277 | assert.match(workspace, /brokerApi\.authenticationAction\(connectionId\)/); |
| 278 | assert.match(workspace, /brokerApi\.listConnections\(\)/); |
| 279 | assert.match(workspace, /portfolioApi\.getDashboard\(\)/); |
| 280 | assert.match(workspace, /portfolioApi\.getPositions\(linkedPortfolio\.portfolioId\)/); |
| 281 | assert.match(workspace, /stopBrokerAuthenticationMonitoring\(true\)/); |
| 282 | assert.match(workspace, /if \(authWindow\.closed\)[\s\S]*?refreshAuthenticatedBroker\(connectionId\)[\s\S]*?stopBrokerAuthenticationMonitoring\(false\)/); |
| 283 | assert.match(workspace, /Waiting for IBKR authentication…/); |
| 284 | assert.match(workspace, /!authenticationPending[\s\S]*?onAuthenticate/); |
| 285 | }); |
| 286 | |
| 287 | test("research uses a clean company name while preserving structured identity", () => { |
| 288 | assert.match(workspace, /function legacyCompositeCompanyName/); |
| 289 | assert.match(workspace, /segments\.length >= 3 && segments\[2\] \? segments\[2\]/); |
| 290 | assert.match(workspace, /position\.customDisplayName\?\.trim\(\)/); |
| 291 | assert.match(workspace, /instrument\.companyName\?\.trim\(\)/); |
| 292 | assert.match(workspace, /instrument\.canonicalName\?\.trim\(\) \|\| resolved\?\.companyName\?\.trim\(\)/); |
| 293 | assert.match(workspace, /ticker: displayTicker,[\s\S]*?exchange: displayExchange/); |
| 294 | assert.match(workspace, /\{option\.companyName\}/); |
| 295 | assert.doesNotMatch(workspace, /label: `\$\{displayTicker\} \/ \$\{displayExchange\} \/ /); |
| 296 | assert.match(workspace, /title=\{option\.companyName\}/); |
| 297 | assert.match(workspace, /join\(" · "\)/); |
| 298 | assert.match(styles, /\.research-company-select option[\s\S]*?background: var\(--research-option-bg\)[\s\S]*?color: var\(--research-option-text\)/); |
| 299 | assert.match(styles, /\.research-company-select option:hover,[\s\S]*?\.research-company-select option:checked/); |
| 300 | assert.match(styles, /\.research-company-select:focus-visible/); |
| 301 | assert.match(styles, /text-overflow: ellipsis/); |
| 302 | }); |