main
mjs 328 lines 18.9 KB
Raw
1 import assert from "node:assert/strict";
2 import { readFileSync } from "node:fs";
3 import test from "node:test";
4 import ts from "typescript";
5
6 const workspace = readFileSync(new URL("../app/components/investment-workspace.tsx", import.meta.url), "utf8");
7 const homePage = readFileSync(new URL("../app/page.tsx", import.meta.url), "utf8");
8 const styles = readFileSync(new URL("../app/styles.css", import.meta.url), "utf8");
9 const portfolioApiSource = readFileSync(new URL("../app/lib/portfolio-api.ts", import.meta.url), "utf8");
10 const marketEnsureReadyDeclaration = workspace.indexOf("const marketEnsureAuthReady =");
11 const marketEnsureCall = workspace.indexOf('portfolioApi.ensureMarketData("INDIA")', marketEnsureReadyDeclaration);
12 const marketEnsureEffectStart = workspace.lastIndexOf(" useEffect(() => {", marketEnsureCall);
13 const marketEnsureEffectEnd = workspace.indexOf("\n }, [marketEnsureAuthReady]);", marketEnsureEffectStart);
14 const marketEnsureEffectBody = workspace.slice(
15 marketEnsureEffectStart + " useEffect(() => {".length,
16 marketEnsureEffectEnd
17 );
18 const marketEnsureEffectJavaScript = ts.transpileModule(`
19 export function runMarketEnsureEffect(
20 marketEnsureAuthReady,
21 portfolioApi,
22 console,
23 marketEnsureErrorCategory
24 ) {
25 ${marketEnsureEffectBody}
26 }
27 `, {
28 compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }
29 }).outputText;
30 const { runMarketEnsureEffect } = await import(
31 `data:text/javascript;base64,${Buffer.from(marketEnsureEffectJavaScript).toString("base64")}`
32 );
33
34 function createMarketEnsureEffectRenderer(ensureMarketData) {
35 let initialized = false;
36 let previousAuthReady;
37 const logs = [];
38 const portfolioApi = { ensureMarketData };
39 const diagnosticConsole = { info: (...args) => logs.push(args) };
40 const category = (error) => typeof error?.status === "number" ? `HTTP_${error.status}` : "UNKNOWN";
41
42 return {
43 logs,
44 render(authReady) {
45 if (initialized && Object.is(previousAuthReady, authReady)) return false;
46 initialized = true;
47 previousAuthReady = authReady;
48 runMarketEnsureEffect(authReady, portfolioApi, diagnosticConsole, category);
49 return true;
50 }
51 };
52 }
53
54 test("every holding row opens provenance-rich stock research details", () => {
55 assert.match(workspace, /onClick=\{\(\) => setDetail\(\{ position, researchInstrumentId: research\?\.instrumentId \?\? position\.instrument\.globalInstrumentId \}\)\}/);
56 for (const section of ["Overview", "Market Data", "Valuation", "Quality / Fundamentals", "Analyst View", "Latest Quarterly Result", "Financial History", "Shareholding Pattern", "Debt & Balance Sheet", "Cash Flow", "Current Quarter Catalysts", "News", "Research & Evidence"]) {
57 assert.match(workspace, new RegExp(`>${section}<`));
58 }
59 assert.match(workspace, /target="_blank" rel="noreferrer"/);
60 assert.match(workspace, /External public analyst consensus; not an application recommendation/);
61 assert.match(workspace, /Provider ticker:/);
62 assert.match(workspace, /Price freshness/);
63 assert.match(workspace, /financialResultHistory/);
64 assert.match(workspace, /balanceSheetHistory/);
65 assert.match(workspace, /cashFlowHistory/);
66 });
67
68 test("holdings keep research fields in the drawer and the table concise", () => {
69 for (const heading of ["P/E", "Valuation", "Latest result", "Shareholding", "Catalyst", "Research"]) {
70 assert.doesNotMatch(workspace, new RegExp(`<th>${heading}<\\/th>`));
71 }
72 for (const heading of ["Company", "Ticker", "Quantity", "Average cost", "Latest price", "Market value", "Unrealized P/L", "Unrealized P/L %", "Allocation", "Currency", "Research & price status"]) {
73 assert.match(workspace, new RegExp(`<th>${heading.replace("/", "\\/")}<\\/th>`));
74 }
75 assert.match(workspace, /research\?\.valuation\.state/);
76 assert.match(workspace, /Not publicly available/);
77 assert.match(workspace, /Awaiting research/);
78 });
79
80 test("holdings present research, imported-price, and live-quote states as distinct text", () => {
81 assert.match(workspace, /function ResearchStatusBadge/);
82 assert.match(workspace, /RESOLVED_RESEARCH_AVAILABLE/);
83 assert.match(workspace, /RESOLVED_PARTIAL_DATA/);
84 assert.match(workspace, /ETF_UNSUPPORTED/);
85 assert.match(workspace, /Research: \{label\}/);
86 assert.match(workspace, /Position price: .*Imported snapshot/);
87 assert.match(workspace, /Live quote:/);
88 assert.match(workspace, /Research price:/);
89 assert.match(workspace, /Valuation:/);
90 assert.match(workspace, /Ownership increase:/);
91 assert.match(workspace, /function ResearchCoverage/);
92 assert.match(workspace, /Research coverage:/);
93 assert.match(workspace, /\{available\} available/);
94 assert.match(workspace, /\{partial\} partial/);
95 assert.match(workspace, /\{etfUnsupported\} ETF unsupported/);
96 assert.match(workspace, /\{fresh\} fresh/);
97 assert.match(workspace, /\{stale\} stale/);
98 });
99
100 test("missing live prices render N/A and never drive market value or profit-loss presentation", () => {
101 assert.match(portfolioApiSource, /currentPrice: Money \| null/);
102 assert.match(portfolioApiSource, /marketValue: Money \| null/);
103 assert.match(portfolioApiSource, /unrealizedProfitLoss: Money \| null/);
104 assert.match(workspace, /return money \? formatMoney\(money\.amount, money\.currency\) : "N\/A"/);
105 assert.match(workspace, /positions\.every\(\(position\) => position\.marketValue && position\.unrealizedProfitLoss\)/);
106 assert.match(workspace, /<td>\{formatBackendMoney\(position\.currentPrice\)\}<\/td>/);
107 assert.match(workspace, /<td>\{formatBackendMoney\(position\.marketValue\)\}<\/td>/);
108 assert.match(workspace, /researchCurrentPrice > 0/);
109 assert.match(workspace, /position\.currentPrice\.amount > 0/);
110 assert.match(workspace, /: "N\/A"/);
111 assert.doesNotMatch(workspace, /position\.currentPrice\.amount, position\.currentPrice\.currency/);
112 });
113
114 test("research readiness stays disabled without a safe canonical global instrument identity", () => {
115 assert.match(workspace, /return Boolean\(globalInstrumentId\?\.trim\(\)\)/);
116 assert.match(workspace, /\["COMPANY_NOT_RESOLVED", "RESEARCH_NOT_APPLICABLE", "ETF_UNSUPPORTED"\]\.includes\(normalizedStatus\)/);
117 assert.match(workspace, /disabled=\{loading \|\| !refreshEligible\}/);
118 });
119
120 test("shareholding drawer renders a dynamic oldest-to-newest four-quarter table without inventing missing values", () => {
121 assert.match(workspace, /function ShareholdingPatternTable/);
122 assert.match(workspace, /\.slice\(0, 4\)\.sort\(\(left, right\) =>/);
123 assert.match(workspace, /new Date\(left\.periodEnd\).*new Date\(right\.periodEnd\)/s);
124 assert.match(workspace, /\["PROMOTER", "Promoters"\]/);
125 assert.match(workspace, /\["FII_FPI", "FII \/ FPI"\]/);
126 assert.match(workspace, /\["DII", "DII"\]/);
127 assert.match(workspace, /\["PUBLIC_RETAIL", "Retail Public"\]/);
128 assert.match(workspace, /\["PROMOTER_PLEDGE", "Promoter Pledge\*"\]/);
129 assert.match(workspace, /Resident individual shareholders holding nominal share capital up to \u20B92 lakh\./);
130 assert.match(workspace, /if \(value === undefined\) return "\u2014"/);
131 assert.match(workspace, /percentage\.toFixed\(2\).*%/);
132 assert.match(workspace, /\* % of promoter holding/);
133 assert.match(workspace, /newest\.sourceProvider} Shareholding XBRL/);
134 assert.match(workspace, /research\?\.shareholdingSnapshots\?\.length \? <ShareholdingPatternTable snapshots=\{research\.shareholdingSnapshots\}/);
135 assert.match(workspace, /: <p>Unavailable\.<\/p>/);
136 assert.doesNotMatch(workspace, /100\s*-\s*other|public.*100\s*-/i);
137 });
138
139 test("shareholding drawers resolve the current portfolio company by instrumentId instead of retaining a stale company copy", () => {
140 assert.match(workspace, /researchInstrumentId: research\?\.instrumentId \?\? position\.instrument\.globalInstrumentId/);
141 assert.match(workspace, /portfolioResearch\?\.companies\.find\(\(company\) => company\.instrumentId === detail\.researchInstrumentId\)/);
142 assert.match(workspace, /researchInstrumentId: company\.instrumentId/);
143 assert.match(workspace, /portfolioResearchSummary\?\.companies\.find\(\(company\) => company\.instrumentId === researchDetail\.researchInstrumentId\)/);
144 assert.doesNotMatch(workspace, /setResearchDetail\(\{ position: holding, research: company \}\)/);
145 });
146
147 test("legacy portfolio refresh UI and polling are removed after readiness cutover", () => {
148 assert.doesNotMatch(workspace, /refreshPortfolio|getRefreshJob|getActiveRefreshJob/);
149 assert.doesNotMatch(workspace, /GlobalResearchRefreshProgress|activeResearchRefreshJob/);
150 assert.doesNotMatch(workspace, /Refresh portfolio research|Updating research/);
151 assert.match(workspace, />\s*Research readiness\s*<\/Button>/);
152 assert.match(workspace, /findResearchData\(requirements\)/);
153 assert.match(workspace, /portfolioResearch\?\.companies\.find\(\(company\) => company\.instrumentId === detail\.researchInstrumentId\)/);
154 });
155
156 test("portfolio summary failure keeps persisted company research independently available", () => {
157 assert.match(workspace, /setPortfolioResearchError\("Portfolio research summary unavailable\. Existing company research remains available\."\)/);
158 assert.match(workspace, /const positionsRef = useRef<PortfolioPosition\[\]>\(\[\]\);/);
159 assert.match(workspace, /positionsRef\.current = positions;/);
160 assert.match(workspace, /setSelectedResearchInstrumentId\(\(current\) => current \|\| positionsRef\.current\.find/);
161 assert.match(workspace, /\{researchContext\.kind === "PORTFOLIO" && portfolioResearchError \? <p className="research-refresh-notice" role="alert">\{portfolioResearchError\}<\/p> : null\}/);
162 assert.match(workspace, /\) : summary \|\| researchContext.kind === "SEARCH" \? null : \(/);
163 const effectStart = workspace.indexOf("async function loadPortfolioResearchSummary()");
164 const effectEnd = workspace.indexOf("async function loadPortfolioDetail()", effectStart);
165 const effect = workspace.slice(effectStart, effectEnd);
166 const failureBranch = effect.slice(effect.indexOf("} catch {"), effect.indexOf("} finally {"));
167 assert.doesNotMatch(failureBranch, /setSelectedResearchInstrumentId\(""\)/);
168 });
169
170 test("portfolio research summary fetch is keyed only by portfolio selection, not positions hydration", () => {
171 const start = workspace.indexOf("async function loadPortfolioResearchSummary()");
172 const end = workspace.indexOf("async function loadPortfolioDetail()", start);
173 const effect = workspace.slice(start, end);
174
175 assert.equal((effect.match(/researchApi\.getPortfolioSummary\(selectedPortfolioId\)/g) ?? []).length, 1);
176 assert.match(effect, /\}, \[selectedPortfolioId\]\);/);
177 assert.doesNotMatch(effect, /\}, \[selectedPortfolioId, positions\]\);/);
178 assert.match(effect, /positionsRef\.current\.find/);
179 });
180
181 test("research intelligence uses category-linked evidence rather than the generic recent-event slice", () => {
182 assert.match(workspace, /function categorySupportingEvents/);
183 assert.match(workspace, /evidence\?\.supportingEvents/);
184 assert.match(workspace, /categorySupportingEvents\(summary, "CAPEX"/);
185 assert.match(workspace, /CAPEX: "CAPEX & Capacity"/);
186 assert.match(workspace, /GUIDANCE: "Guidance"/);
187 });
188
189 test("ownership border combinations are independent and reduced-motion safe", () => {
190 for (const name of ["promoter", "fii", "dii", "promoter-dii", "promoter-fii", "fii-dii", "promoter-fii-dii"]) {
191 assert.match(styles, new RegExp(`\\.ownership-${name}\\b`));
192 }
193 assert.match(styles, /@keyframes ownership-dots/);
194 assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*ownership-/);
195 for (const state of ["cheap", "fair", "expensive", "unknown"]) {
196 assert.match(styles, new RegExp(`valuation-${state}`));
197 }
198 });
199
200 test("explicit INR research metrics use canonical compact Indian monetary display without relabeling foreign values", () => {
201 assert.match(workspace, /function metricDisplay/);
202 assert.match(workspace, /unit\?\.toUpperCase\(\) === "INR"/);
203 assert.match(workspace, /numeric \/ 10_000_000\).*Cr/s);
204 assert.match(workspace, /numeric \/ 100_000\).*Lakh/s);
205 assert.match(workspace, /new Intl\.NumberFormat\("en-IN"/);
206 assert.match(workspace, /title: exact/);
207 assert.match(workspace, /unit && unit !== "ratio"/);
208 assert.match(workspace, /<MetricValue metric=\{period\.revenue\}/);
209 assert.match(workspace, /<MetricValue metric=\{period\.pat\}/);
210 assert.match(workspace, /<MetricValue metric=\{keys\.map/);
211 });
212
213 test("dashboard sector performance is backend-authoritative and holdings presentation is absent", () => {
214 assert.match(workspace, /portfolioApi\.getSectorPerformance\(sectorPerformanceRegion, sectorPerformanceSector, sectorPerformancePeriod\)/);
215 assert.match(workspace, />Sector Performance</);
216 assert.match(workspace, />Top 5 Performers</);
217 assert.match(workspace, />Worst 5 Performers</);
218 assert.match(workspace, /aria-label="Sector performance region"/);
219 assert.match(workspace, /aria-label="Sector performance sector"/);
220 assert.match(workspace, /aria-label="Sector performance period"/);
221 assert.match(workspace, /onClick=\{\(\) => onOpenResearch\(stock\)\}/);
222 const start = workspace.indexOf("function MultiPortfolioDashboard");
223 const end = workspace.indexOf("function PortfolioCreatePanel", start);
224 const dashboard = workspace.slice(start, end);
225 assert.doesNotMatch(dashboard, /\.sort\(/);
226 assert.doesNotMatch(dashboard, /\.slice\(0,\s*5\)/);
227 assert.doesNotMatch(dashboard, /All holdings|combinedHoldings|Matching ISINs are combined/);
228 assert.doesNotMatch(dashboard, /Score \{?stock/);
229 });
230
231 test("market-data ensure uses a dedicated primitive-auth effect and the shared authenticated API route", () => {
232 assert.notEqual(marketEnsureReadyDeclaration, -1);
233 assert.notEqual(marketEnsureEffectStart, -1);
234 assert.notEqual(marketEnsureEffectEnd, -1);
235 assert.match(workspace, /const marketEnsureAuthReady = authenticatedUser !== null && Boolean\(accessToken\);/);
236 assert.match(marketEnsureEffectBody, /portfolioApi\.ensureMarketData\("INDIA"\)/);
237 assert.match(marketEnsureEffectBody, /event: "EFFECT", authReady: marketEnsureAuthReady/);
238 assert.match(marketEnsureEffectBody, /event: "DISPATCH"/);
239 assert.match(marketEnsureEffectBody, /event: "RESOLVED"/);
240 assert.match(marketEnsureEffectBody, /event: "REJECTED", category:/);
241 assert.doesNotMatch(marketEnsureEffectBody, /sectorPerformanceRegion|sectorPerformanceSector|sectorPerformancePeriod|getSectorPerformance/);
242 assert.match(workspace.slice(marketEnsureEffectEnd), /^\n \}, \[marketEnsureAuthReady\]\);/);
243 assert.doesNotMatch(workspace, /marketDataEnsureDispatchedRef|oneShotGuard|ensureIndiaMarketDataForAuthenticatedUser|market-data-ensure-lifecycle|onDispatched/);
244
245 assert.match(portfolioApiSource, /ensureMarketData: \(region: "INDIA"\) =>[\s\S]*?\/api\/v1\/research\/market-data\/ensure\?region=\$\{region\}[\s\S]*?method: "POST"/);
246 assert.match(portfolioApiSource, /async function request<T>\(path: string, init\?: RequestInit\)[\s\S]*?const token = currentAuthenticatedApiToken\(\)[\s\S]*?Authorization: `Bearer \$\{token\}`/);
247 assert.doesNotMatch(portfolioApiSource, /onDispatched|responsePromise/);
248 });
249
250 test("the root Dashboard route mounts the sole runtime owner of Sector Performance and market-data ensure", () => {
251 assert.match(homePage, /import \{ InvestmentWorkspace \} from "\.\/components\/investment-workspace";/);
252 assert.match(homePage, /return <InvestmentWorkspace \/>;/);
253 assert.equal((workspace.match(/portfolioApi\.getSectorPerformance\(/g) ?? []).length, 1);
254 assert.equal((workspace.match(/portfolioApi\.ensureMarketData\("INDIA"\)/g) ?? []).length, 1);
255 assert.ok(marketEnsureCall < workspace.indexOf("portfolioApi.getSectorPerformance("));
256 });
257
258 test("successful login reloads the active root build before authenticated Dashboard effects mount", () => {
259 const loginStart = workspace.indexOf("async function login(");
260 const loginEnd = workspace.indexOf("function logout()", loginStart);
261 const loginPaths = workspace.slice(loginStart, loginEnd);
262
263 assert.equal((loginPaths.match(/window\.location\.replace\("\/"\);/g) ?? []).length, 2);
264 assert.doesNotMatch(loginPaths, /setAccessToken\(session\.accessToken\)/);
265 assert.doesNotMatch(loginPaths, /setAuthenticatedUser\(session\.user\)/);
266 });
267
268 test("authReady false performs zero INDIA ensure calls", () => {
269 let calls = 0;
270 const renderer = createMarketEnsureEffectRenderer(async () => { calls += 1; });
271 assert.equal(renderer.render(false), true);
272 assert.equal(calls, 0);
273 assert.deepEqual(renderer.logs[0], ["[AIP_MARKET_ENSURE]", { event: "EFFECT", authReady: false }]);
274 });
275
276 test("false-to-true auth transition dispatches exactly once", () => {
277 let calls = 0;
278 const renderer = createMarketEnsureEffectRenderer(async () => { calls += 1; });
279 renderer.render(false);
280 renderer.render(true);
281 assert.equal(calls, 1);
282 assert.equal(renderer.logs.some((entry) => entry[1]?.event === "DISPATCH"), true);
283 });
284
285 test("initially authenticated mount dispatches ensure", () => {
286 let calls = 0;
287 const renderer = createMarketEnsureEffectRenderer(async () => { calls += 1; });
288 renderer.render(true);
289 assert.equal(calls, 1);
290 });
291
292 test("ordinary rerenders and Region Sector Period changes do not retrigger ensure", () => {
293 let calls = 0;
294 const renderer = createMarketEnsureEffectRenderer(async () => { calls += 1; });
295 renderer.render(true);
296 for (const unrelatedChange of ["rerender", "INDIA", "Financials", "DAY", "Technology", "YEAR"]) {
297 assert.ok(unrelatedChange);
298 assert.equal(renderer.render(true), false);
299 }
300 assert.equal(calls, 1);
301 });
302
303 test("logout and subsequent login produce a new false-to-true lifecycle", () => {
304 let calls = 0;
305 const renderer = createMarketEnsureEffectRenderer(async () => { calls += 1; });
306 renderer.render(true);
307 renderer.render(false);
308 assert.equal(calls, 1);
309 renderer.render(true);
310 assert.equal(calls, 2);
311 });
312
313 test("rejected ensure remains non-blocking and reports only a safe category", async () => {
314 const renderer = createMarketEnsureEffectRenderer(() => Promise.reject({ status: 503 }));
315 assert.doesNotThrow(() => renderer.render(true));
316 await new Promise((resolve) => setImmediate(resolve));
317 assert.deepEqual(renderer.logs.at(-1), ["[AIP_MARKET_ENSURE]", { event: "REJECTED", category: "HTTP_503" }]);
318 assert.match(workspace, /const dashboard = await portfolioApi\.getDashboard\(\)/);
319 });
320
321 test("Sector Performance waits for a backend-authoritative valid sector selection", () => {
322 assert.match(workspace, /portfolioApi\.getSectorPerformance\(sectorPerformanceRegion, sectorPerformanceSector, sectorPerformancePeriod\)/);
323 assert.match(workspace, /portfolioApi\.getMarketUniverseSectors\(sectorPerformanceRegion\)/);
324 assert.match(workspace, /sectorOptions\.some\(\(option\) => option\.name === sectorPerformanceSector\)/);
325 assert.match(workspace, /\[accessToken, authenticatedUser, sectorOptions, sectorOptionsRegion, sectorPerformanceRegion, sectorPerformanceSector, sectorPerformancePeriod\]/);
326 assert.match(portfolioApiSource, /getSectorPerformance:[\s\S]*?request<SectorPerformance>\(`\/api\/v1\/research\/sector-performance/);
327 assert.equal((workspace.match(/portfolioApi\.ensureMarketData\("INDIA"\)/g) ?? []).length, 1);
328 });