test: mock AnthropicClient in analyze tests to prevent live API calls

This commit is contained in:
Kazuma
2026-06-08 12:08:37 -04:00
parent 76c2a671f4
commit ad1c3fe3c9
31 changed files with 415 additions and 171 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import Spinner from '$lib/Spinner.svelte';
import Spinner from '$lib/components/shared/Spinner.svelte';
interface FormData {
title: string;
+2
View File
@@ -0,0 +1,2 @@
export * from './shared/index.js';
export * from './screener/index.js';
@@ -1,5 +1,5 @@
<script lang="ts">
import Spinner from '$lib/Spinner.svelte';
import Spinner from '$lib/components/shared/Spinner.svelte';
import type { SidebarState } from '$lib/types.js';
let { sidebar, onClose }: { sidebar: SidebarState; onClose: () => void } = $props();
@@ -1,8 +1,8 @@
<script lang="ts">
import { sigOrd, sorted } from '$lib/utils.js';
import VerdictPill from '$lib/VerdictPill.svelte';
import SignalBadge from '$lib/SignalBadge.svelte';
import Spinner from '$lib/Spinner.svelte';
import VerdictPill from '$lib/components/shared/VerdictPill.svelte';
import SignalBadge from '$lib/components/shared/SignalBadge.svelte';
import Spinner from '$lib/components/shared/Spinner.svelte';
import type { AssetType, AssetResult } from '$lib/types.js';
let {
+2
View File
@@ -0,0 +1,2 @@
export { default as AssetTable } from './AssetTable.svelte';
export { default as AnalysisSidebar } from './AnalysisSidebar.svelte';
+5
View File
@@ -0,0 +1,5 @@
export { default as Spinner } from './Spinner.svelte';
export { default as VerdictPill } from './VerdictPill.svelte';
export { default as SignalBadge } from './SignalBadge.svelte';
export { default as MarketContext } from './MarketContext.svelte';
export { default as MarketContextStrip } from './MarketContextStrip.svelte';
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import SignalBadge from '$lib/SignalBadge.svelte';
import SignalBadge from '$lib/components/shared/SignalBadge.svelte';
import { sigOrd, fmt, fmtShort, glClass, advClass } from '$lib/utils.js';
import type { AdviceRow } from '$lib/types.js';
+4 -103
View File
@@ -1,105 +1,6 @@
/**
* Shared pure utility functions used across screener, portfolio, and safe-buys pages.
* All functions are stateless and framework-agnostic.
* Backward-compatibility shim.
* New code should import from '$lib/utils/index.js' or the specific submodule.
* Existing '$lib/utils.js' imports continue to work unchanged.
*/
// ── Signal ordering ───────────────────────────────────────────────────────────
export type Signal =
| '✅ Strong Buy'
| '⚡ Momentum'
| '🔄 Neutral'
| '⚠️ Speculation'
| '❌ Avoid';
const SIGNAL_ORDER: Record<string, number> = {
'✅ Strong Buy': 0,
'⚡ Momentum': 1,
'🔄 Neutral': 2,
'⚠️ Speculation': 3,
'❌ Avoid': 4,
};
/** Returns sort order for a signal string (lower = stronger). Unknown signals → 5. */
export function sigOrd(signal: string | null | undefined): number {
return SIGNAL_ORDER[signal ?? ''] ?? 5;
}
/** Sorts an array of screener result rows by signal strength (strongest first). */
export function sorted<T extends { signal?: string | null }>(arr: T[]): T[] {
return [...(arr ?? [])].sort((a, b) => sigOrd(a.signal) - sigOrd(b.signal));
}
// ── Verdict label helpers ─────────────────────────────────────────────────────
/**
* Converts a long verdict label into a short display string.
* e.g. "🟢 BUY (High Conviction)" → "Strong"
*/
export function verdictShort(label: string | null | undefined): string {
if (!label) return '—';
if (label.includes('High Conviction')) return 'Strong';
if (label.includes('Speculative')) return 'Speculative';
if (label.includes('BUY')) return 'Buy';
if (label.includes('Efficient')) return 'Efficient';
if (label.includes('Attractive')) return 'Attractive';
if (label.includes('Neutral')) return 'Hold';
if (label.includes('REJECT')) return 'Reject';
if (label.includes('Avoid')) return 'Avoid';
return label.replace(/[🟢🟡🔴]/u, '').trim();
}
/**
* Returns a CSS colour class ('green' | 'yellow' | 'red') based on
* the emoji prefix of a verdict label.
*/
export function vClass(label: string | null | undefined): 'green' | 'yellow' | 'red' {
if (label?.startsWith('🟢')) return 'green';
if (label?.startsWith('🟡')) return 'yellow';
return 'red';
}
// ── Number formatters ─────────────────────────────────────────────────────────
/** Formats a P/E ratio — e.g. 22.5 → "22.5x", null → "—" */
export function fmtPE(v: number | null | undefined): string {
return v != null ? v + 'x' : '—';
}
/** Full currency format — e.g. 1234.5 → "$1,234.50" */
export function fmt(n: number | null | undefined): string {
return n != null
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n)
: '—';
}
/** Compact currency format (no cents) — e.g. 1234.5 → "$1,235" */
export function fmtShort(n: number | null | undefined): string {
return n != null
? new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}).format(n)
: '—';
}
/**
* Returns 'green' for non-negative G/L percentage, 'red' otherwise.
* Accepts string (e.g. "12.5") or number.
*/
export function glClass(pct: string | number | null | undefined): 'green' | 'red' {
return parseFloat(String(pct ?? 0)) >= 0 ? 'green' : 'red';
}
/**
* Returns a CSS colour class for a portfolio advice string based on its emoji prefix.
* 🟢 → 'green', 🟡 → 'yellow', 🟠 → 'orange', 🔴 → 'red', else 'gray'.
*/
export function advClass(advice: string | null | undefined): 'green' | 'yellow' | 'orange' | 'red' | 'gray' {
if (advice?.includes('🟢')) return 'green';
if (advice?.includes('🟡')) return 'yellow';
if (advice?.includes('🟠')) return 'orange';
if (advice?.includes('🔴')) return 'red';
return 'gray';
}
export * from './utils/index.js';
+26
View File
@@ -0,0 +1,26 @@
/**
* Number and currency formatting utilities.
*/
/** Formats a P/E ratio — e.g. 22.5 → "22.5x", null → "—" */
export function fmtPE(v: number | null | undefined): string {
return v != null ? v + 'x' : '—';
}
/** Full currency format — e.g. 1234.5 → "$1,234.50" */
export function fmt(n: number | null | undefined): string {
return n != null
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n)
: '—';
}
/** Compact currency format (no cents) — e.g. 1234.5 → "$1,235" */
export function fmtShort(n: number | null | undefined): string {
return n != null
? new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
}).format(n)
: '—';
}
+3
View File
@@ -0,0 +1,3 @@
export * from './sorting.js';
export * from './verdicts.js';
export * from './formatting.js';
+28
View File
@@ -0,0 +1,28 @@
/**
* Signal ordering and sorting utilities.
*/
export type Signal =
| '✅ Strong Buy'
| '⚡ Momentum'
| '🔄 Neutral'
| '⚠️ Speculation'
| '❌ Avoid';
const SIGNAL_ORDER: Record<string, number> = {
'✅ Strong Buy': 0,
'⚡ Momentum': 1,
'🔄 Neutral': 2,
'⚠️ Speculation': 3,
'❌ Avoid': 4,
};
/** Returns sort order for a signal string (lower = stronger). Unknown signals → 5. */
export function sigOrd(signal: string | null | undefined): number {
return SIGNAL_ORDER[signal ?? ''] ?? 5;
}
/** Sorts an array of screener result rows by signal strength (strongest first). */
export function sorted<T extends { signal?: string | null }>(arr: T[]): T[] {
return [...(arr ?? [])].sort((a, b) => sigOrd(a.signal) - sigOrd(b.signal));
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Verdict label helpers — convert long verdict strings to short display values
* and derive CSS colour classes from emoji prefixes.
*/
/**
* Converts a long verdict label into a short display string.
* e.g. "🟢 BUY (High Conviction)" → "Strong"
*/
export function verdictShort(label: string | null | undefined): string {
if (!label) return '—';
if (label.includes('High Conviction')) return 'Strong';
if (label.includes('Speculative')) return 'Speculative';
if (label.includes('BUY')) return 'Buy';
if (label.includes('Efficient')) return 'Efficient';
if (label.includes('Attractive')) return 'Attractive';
if (label.includes('Neutral')) return 'Hold';
if (label.includes('REJECT')) return 'Reject';
if (label.includes('Avoid')) return 'Avoid';
return label.replace(/[🟢🟡🔴]/u, '').trim();
}
/**
* Returns a CSS colour class ('green' | 'yellow' | 'red') based on
* the emoji prefix of a verdict label.
*/
export function vClass(label: string | null | undefined): 'green' | 'yellow' | 'red' {
if (label?.startsWith('🟢')) return 'green';
if (label?.startsWith('🟡')) return 'yellow';
return 'red';
}
/**
* Returns a CSS colour class for a portfolio advice string based on its emoji prefix.
* 🟢 → 'green', 🟡 → 'yellow', 🟠 → 'orange', 🔴 → 'red', else 'gray'.
*/
export function advClass(
advice: string | null | undefined,
): 'green' | 'yellow' | 'orange' | 'red' | 'gray' {
if (advice?.includes('🟢')) return 'green';
if (advice?.includes('🟡')) return 'yellow';
if (advice?.includes('🟠')) return 'orange';
if (advice?.includes('🔴')) return 'red';
return 'gray';
}
/**
* Returns 'green' for non-negative G/L percentage, 'red' otherwise.
* Accepts string (e.g. "12.5") or number.
*/
export function glClass(pct: string | number | null | undefined): 'green' | 'red' {
return parseFloat(String(pct ?? 0)) >= 0 ? 'green' : 'red';
}