main
mjs 70 lines 3.74 KB
Raw
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 });