2026-06-29 21:31:59 -04:00
|
|
|
import { describe, it } from "node:test";
|
|
|
|
|
import * as assert from "node:assert/strict";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
STARTER_WATCHLIST,
|
2026-08-10 13:36:26 -04:00
|
|
|
STARTER_PACK_FOCUSED,
|
|
|
|
|
STARTER_PACK_STANDARD,
|
2026-06-29 21:31:59 -04:00
|
|
|
ONBOARDING_DISCLAIMER,
|
|
|
|
|
defaultDrawdownTolerancePct,
|
|
|
|
|
defaultRiskTolerance,
|
2026-08-10 13:36:26 -04:00
|
|
|
starterPackFor,
|
|
|
|
|
resolveWorkspaceDefaults,
|
|
|
|
|
complexityFromExperience,
|
|
|
|
|
defaultDensity,
|
2026-06-29 21:31:59 -04:00
|
|
|
type StarterSymbol,
|
|
|
|
|
} from "../starter.ts";
|
|
|
|
|
|
2026-08-10 13:36:26 -04:00
|
|
|
const FORBIDDEN = [
|
|
|
|
|
/buy\b/i,
|
|
|
|
|
/sell\b/i,
|
|
|
|
|
/you should\b/i,
|
|
|
|
|
/add to your\b/i,
|
|
|
|
|
/rotate into\b/i,
|
|
|
|
|
/action needed\b/i,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function assertNoTradeVerbs(items: StarterSymbol[], label: string) {
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
const hit = FORBIDDEN.find((rx) => rx.test(item.reason));
|
|
|
|
|
assert.equal(hit, undefined, `${label}: forbidden verb in reason for ${item.symbol}`);
|
|
|
|
|
assert.ok(item.reason.length > 0, `${label}: empty reason for ${item.symbol}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 21:31:59 -04:00
|
|
|
describe("onboarding/starter", () => {
|
2026-08-10 13:36:26 -04:00
|
|
|
describe("STARTER_PACK_FOCUSED / STARTER_WATCHLIST", () => {
|
|
|
|
|
it("focused pack has liquid exemplars (no thin microcaps as default)", () => {
|
|
|
|
|
const symbols = STARTER_PACK_FOCUSED.map((s) => s.symbol);
|
|
|
|
|
assert.ok(symbols.includes("VTI"));
|
|
|
|
|
assert.ok(symbols.includes("AAPL"));
|
|
|
|
|
assert.ok(!symbols.includes("IREN"));
|
|
|
|
|
assert.ok(!symbols.includes("CIFR"));
|
|
|
|
|
assert.equal(STARTER_WATCHLIST.length, STARTER_PACK_FOCUSED.length);
|
2026-06-29 21:31:59 -04:00
|
|
|
});
|
|
|
|
|
|
2026-08-10 13:36:26 -04:00
|
|
|
it("every focused entry has tickerKind and reason without trade verbs", () => {
|
|
|
|
|
assertNoTradeVerbs(STARTER_PACK_FOCUSED, "focused");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("starterPackFor", () => {
|
|
|
|
|
it("standard includes focused symbols plus benchmarks", () => {
|
|
|
|
|
const pack = starterPackFor("standard");
|
|
|
|
|
const symbols = pack.map((s) => s.symbol);
|
|
|
|
|
assert.ok(symbols.includes("VTI"));
|
|
|
|
|
assert.ok(symbols.includes("SPY"));
|
|
|
|
|
assert.ok(symbols.includes("NVDA"));
|
|
|
|
|
assertNoTradeVerbs(pack, "standard");
|
2026-06-29 21:31:59 -04:00
|
|
|
});
|
|
|
|
|
|
2026-08-10 13:36:26 -04:00
|
|
|
it("full adds optional higher-vol research examples", () => {
|
|
|
|
|
const pack = starterPackFor("full");
|
|
|
|
|
const symbols = pack.map((s) => s.symbol);
|
|
|
|
|
assert.ok(symbols.includes("SPY"));
|
|
|
|
|
assert.ok(symbols.includes("IREN") || symbols.includes("BTC"));
|
|
|
|
|
assertNoTradeVerbs(pack, "full");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("resolveWorkspaceDefaults", () => {
|
|
|
|
|
it("maps never_invested → beginner + focused + focused pack", () => {
|
|
|
|
|
const d = resolveWorkspaceDefaults({ experienceStage: "never_invested" });
|
|
|
|
|
assert.equal(d.complexity, "beginner");
|
|
|
|
|
assert.equal(d.density, "focused");
|
|
|
|
|
assert.equal(d.drawdownTolerancePct, -20);
|
|
|
|
|
assert.deepEqual(
|
|
|
|
|
d.starterPack.map((s) => s.symbol),
|
|
|
|
|
STARTER_PACK_FOCUSED.map((s) => s.symbol),
|
|
|
|
|
);
|
2026-06-29 21:31:59 -04:00
|
|
|
});
|
|
|
|
|
|
2026-08-10 13:36:26 -04:00
|
|
|
it("maps active_self_directed → advanced + full", () => {
|
|
|
|
|
const d = resolveWorkspaceDefaults({ experienceStage: "active_self_directed" });
|
|
|
|
|
assert.equal(d.complexity, "advanced");
|
|
|
|
|
assert.equal(d.density, "full");
|
|
|
|
|
assert.equal(d.riskTolerance, "aggressive");
|
2026-06-29 21:31:59 -04:00
|
|
|
});
|
|
|
|
|
|
2026-08-10 13:36:26 -04:00
|
|
|
it("respects explicit density override", () => {
|
|
|
|
|
const d = resolveWorkspaceDefaults({
|
|
|
|
|
experienceStage: "never_invested",
|
|
|
|
|
density: "standard",
|
|
|
|
|
goal: "protect",
|
|
|
|
|
horizon: "short",
|
|
|
|
|
});
|
|
|
|
|
assert.equal(d.density, "standard");
|
|
|
|
|
assert.equal(d.goal, "protect");
|
|
|
|
|
assert.equal(d.horizon, "short");
|
|
|
|
|
assert.ok(d.starterPack.length >= STARTER_PACK_STANDARD.length - 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("complexityFromExperience / defaultDensity", () => {
|
|
|
|
|
it("maps stages", () => {
|
|
|
|
|
assert.equal(complexityFromExperience("never_invested"), "beginner");
|
|
|
|
|
assert.equal(complexityFromExperience("some_stocks"), "intermediate");
|
|
|
|
|
assert.equal(complexityFromExperience("active_self_directed"), "advanced");
|
|
|
|
|
assert.equal(defaultDensity("never_invested"), "focused");
|
|
|
|
|
assert.equal(defaultDensity("some_stocks"), "standard");
|
|
|
|
|
assert.equal(defaultDensity("active_self_directed"), "full");
|
2026-06-29 21:31:59 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("defaultDrawdownTolerancePct", () => {
|
2026-08-10 13:36:26 -04:00
|
|
|
it("returns expected levels", () => {
|
2026-06-29 21:31:59 -04:00
|
|
|
assert.equal(defaultDrawdownTolerancePct("beginner"), -20);
|
|
|
|
|
assert.equal(defaultDrawdownTolerancePct("intermediate"), -30);
|
|
|
|
|
assert.equal(defaultDrawdownTolerancePct("advanced"), -40);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("defaultRiskTolerance", () => {
|
2026-08-10 13:36:26 -04:00
|
|
|
it("returns expected levels", () => {
|
2026-06-29 21:31:59 -04:00
|
|
|
assert.equal(defaultRiskTolerance("beginner"), "moderate");
|
|
|
|
|
assert.equal(defaultRiskTolerance("intermediate"), "moderate");
|
|
|
|
|
assert.equal(defaultRiskTolerance("advanced"), "aggressive");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("ONBOARDING_DISCLAIMER", () => {
|
2026-08-10 13:36:26 -04:00
|
|
|
it("is short operational setup copy", () => {
|
|
|
|
|
assert.ok(ONBOARDING_DISCLAIMER.length > 0);
|
|
|
|
|
assert.ok(ONBOARDING_DISCLAIMER.toLowerCase().includes("watchlist"));
|
|
|
|
|
assert.ok(!ONBOARDING_DISCLAIMER.toLowerCase().includes("educational"));
|
2026-06-29 21:31:59 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|