| 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 | const runStart = workspace.indexOf("async function runStockRuleEngineAnalysis"); |
| 9 | const runEnd = workspace.indexOf("function openMarketIntelligenceResearch", runStart); |
| 10 | const runPath = workspace.slice(runStart, runEnd); |
| 11 | const breakdownStart = workspace.indexOf("function StockRuleEngineBreakdown"); |
| 12 | const breakdownEnd = workspace.indexOf("function readinessStatusTone", breakdownStart); |
| 13 | const breakdown = workspace.slice(breakdownStart, breakdownEnd); |
| 14 | |
| 15 | test("Run Analysis uses the provider-free deterministic analysis endpoint", () => { |
| 16 | assert.match(api, /analyze: \(globalInstrumentId: string, allowPartial: boolean\) =>[\s\S]*?\/api\/v1\/research\/analysis\/\$\{globalInstrumentId\}[\s\S]*?method: "POST"/); |
| 17 | assert.match(runPath, /researchApi\.analyze\(dialog\.globalInstrumentId, allowPartial\)/); |
| 18 | assert.doesNotMatch(runPath, /NSE|Yahoo|SEC|EODHD|provider|refresh|ensureReadiness|prefetch/); |
| 19 | }); |
| 20 | |
| 21 | test("backend eligibility exclusively controls full and partial analysis actions", () => { |
| 22 | assert.match(workspace, /analysisEligibility\?\.fullAnalysisAllowed/); |
| 23 | assert.match(workspace, /onRunAnalysis\(false\)/); |
| 24 | assert.match(workspace, /analysisEligibility\?\.partialAnalysisAllowed/); |
| 25 | assert.match(workspace, /onRunAnalysis\(true\)/); |
| 26 | }); |
| 27 | |
| 28 | test("score summary renders decision, quality, opportunity, risk, and separate confidence", () => { |
| 29 | assert.match(breakdown, /analysis\.overallScore/); |
| 30 | assert.match(breakdown, /analysis\.decisionSignal/); |
| 31 | assert.match(breakdown, />Quality</); |
| 32 | assert.match(breakdown, />Opportunity</); |
| 33 | assert.match(breakdown, />Risk</); |
| 34 | assert.match(breakdown, /analysis\.confidence} · \{analysis\.confidenceScore/); |
| 35 | assert.doesNotMatch(breakdown, /overallScore\s*\*\s*confidence|confidenceScore\s*\*\s*overall/); |
| 36 | }); |
| 37 | |
| 38 | test("all explainable areas render weight, score, contribution, status, and details", () => { |
| 39 | assert.match(breakdown, /analysis\.areaScores\.map/); |
| 40 | assert.match(breakdown, /area\.weight/); |
| 41 | assert.match(breakdown, /area\.rawScore/); |
| 42 | assert.match(breakdown, /area\.weightedContribution/); |
| 43 | assert.match(breakdown, /area\.status/); |
| 44 | assert.match(breakdown, /area\.positiveFactors/); |
| 45 | assert.match(breakdown, /area\.negativeFactors/); |
| 46 | assert.match(breakdown, /area\.missingInputs/); |
| 47 | }); |
| 48 | |
| 49 | test("metric provenance links are generic and open in a safe new tab", () => { |
| 50 | assert.match(breakdown, /metric\.source/); |
| 51 | assert.match(breakdown, /metric\.rule/); |
| 52 | assert.match(breakdown, /href=\{metric\.sourceUrl\} target="_blank" rel="noreferrer">View source/); |
| 53 | assert.match(breakdown, /area\.sourceReferences/); |
| 54 | assert.match(breakdown, /href=\{source\.sourceUrl\} target="_blank" rel="noreferrer"/); |
| 55 | assert.doesNotMatch(breakdown, /sourceProvider\s*===|NSE|Yahoo|SEC|EODHD/); |
| 56 | }); |
| 57 | |
| 58 | test("risk overrides are visibly presented above the area table", () => { |
| 59 | assert.ok(breakdown.indexOf("rule-engine-overrides") < breakdown.indexOf("rule-engine-area-table")); |
| 60 | assert.match(breakdown, /override\.severity/); |
| 61 | assert.match(breakdown, /override\.effect/); |
| 62 | assert.match(styles, /\.rule-engine-overrides/); |
| 63 | }); |
| 64 | |
| 65 | test("frontend has presentation semantics but no scoring, LLM, or provider rules", () => { |
| 66 | assert.doesNotMatch(breakdown, /trailingPE|ROCE|debtToEquity|CAGR|prompt|LLM|MCP/); |
| 67 | assert.match(styles, /\.rule-engine-result-ready/); |
| 68 | assert.match(styles, /\.rule-engine-result-danger/); |
| 69 | assert.match(api, /ruleEngineVersion: string/); |
| 70 | }); |