harb/tests/e2e/usertest/test-a-passive-holder.spec.ts

682 lines
29 KiB
TypeScript
Raw Normal View History

/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
2026-02-18 00:19:05 +01:00
import { expect, test } from '@playwright/test';
import { Wallet } from 'ethers';
import { createWalletContext } from '../../setup/wallet-provider';
import { getStackConfig, validateStackHealthy } from '../../setup/stack';
import {
createPersonaFeedback,
addFeedbackStep,
writePersonaFeedback,
mintEth,
buyKrk,
resetChainState,
type PersonaFeedback,
} from './helpers';
import { mkdirSync, readFileSync } from 'fs';
import { join } from 'path';
const KRK_ADDRESS = JSON.parse(readFileSync(join(process.cwd(), 'onchain', 'deployments-local.json'), 'utf-8')).contracts.Kraiken.toLowerCase();
const STACK_CONFIG = getStackConfig();
const STACK_RPC_URL = STACK_CONFIG.rpcUrl;
const STACK_WEBAPP_URL = STACK_CONFIG.webAppUrl;
const LANDING_PAGE_URL = 'http://localhost:8081/';
// Account for tests
const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const ACCOUNT_ADDRESS = new Wallet(ACCOUNT_PRIVATE_KEY).address;
test.describe('Test A: Passive Holder Journey', () => {
test.beforeAll(async () => {
await resetChainState(STACK_RPC_URL);
await validateStackHealthy(STACK_CONFIG);
});
test.describe.serial('Tyler - Retail Degen ("sell me in 30 seconds")', () => {
let feedback: PersonaFeedback;
test.beforeAll(() => {
feedback = createPersonaFeedback('tyler', 'A', 'passive-holder');
});
test('Tyler evaluates landing page value prop', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
// Step 1: Navigate to landing page
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
// Tyler's quick evaluation
observations.push('Scanning page... do I see APY numbers? Big buttons? What\'s the hook?');
// Check for value prop clarity
const hasGetKrkButton = await page.getByRole('button', { name: /get.*krk/i }).isVisible().catch(() => false);
if (hasGetKrkButton) {
observations.push('✓ "Get KRK" button is visible and prominent - good CTA');
} else {
observations.push('✗ No clear "Get KRK" button visible - where do I start?');
}
// Check for stats/numbers that catch attention
const hasStats = await page.locator('text=/\\d+%|\\$\\d+|APY/i').first().isVisible().catch(() => false);
if (hasStats) {
observations.push('✓ Numbers visible - I see stats, that\'s good for credibility');
} else {
observations.push('✗ No flashy APY or TVL numbers - nothing to grab my attention');
}
// Crypto jargon check
const pageText = await page.textContent('body') || '';
const jargonWords = ['harberger', 'vwap', 'tokenomics', 'liquidity', 'leverage'];
const foundJargon = jargonWords.filter(word => pageText.toLowerCase().includes(word));
if (foundJargon.length > 2) {
observations.push(`⚠ Too much jargon: ${foundJargon.join(', ')} - might scare normies away`);
} else {
observations.push('✓ Copy is relatively clean, not too technical');
}
// Protocol Health section
const hasProtocolHealth = await page.getByText(/protocol health|system status/i).isVisible().catch(() => false);
if (hasProtocolHealth) {
observations.push('✓ Protocol Health section builds trust - shows transparency');
} else {
observations.push('Missing: No visible protocol health/stats - how do I know this isn\'t rugpull?');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'tyler-a');
mkdirSync(screenshotDir, { recursive: true });
const screenshotPath = join(screenshotDir, `landing-page-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'landing-page', observations, screenshotPath);
// Tyler's 30-second verdict
const verdict = hasGetKrkButton && hasStats ?
'PASS: Clear CTA, visible stats. I\'d click through to learn more.' :
'FAIL: Not sold in 30 seconds. Needs bigger numbers and clearer value prop.';
observations.push(`Tyler\'s verdict: ${verdict}`);
} finally {
await context.close();
}
});
test('Tyler clicks Get KRK and checks Uniswap link', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1_000);
// Click Get KRK button
const getKrkButton = page.getByRole('button', { name: /get.*krk/i }).first();
const buttonVisible = await getKrkButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (buttonVisible) {
await getKrkButton.click();
await page.waitForTimeout(2_000);
// Check if navigated to web-app
const currentUrl = page.url();
if (currentUrl.includes('get-krk') || currentUrl.includes('5173')) {
observations.push('✓ Get KRK button navigated to web-app');
} else {
observations.push(`✗ Get KRK button went to wrong place: ${currentUrl}`);
}
// Check for Uniswap link with correct token address
const uniswapLink = await page.locator(`a[href*="uniswap"][href*="${KRK_ADDRESS}"]`).isVisible().catch(() => false);
if (uniswapLink) {
observations.push('✓ Uniswap link exists with correct KRK token address');
} else {
const anyUniswapLink = await page.locator('a[href*="uniswap"]').isVisible().catch(() => false);
if (anyUniswapLink) {
observations.push('⚠ Uniswap link exists but may have wrong token address');
} else {
observations.push('✗ No Uniswap link found - how do I actually get KRK?');
}
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'tyler-a');
const screenshotPath = join(screenshotDir, `get-krk-page-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'get-krk', observations, screenshotPath);
} else {
observations.push('✗ CRITICAL: Get KRK button not found on landing page');
addFeedbackStep(feedback, 'get-krk', observations);
}
} finally {
await context.close();
}
});
test('Tyler simulates having KRK and checks return value', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
// Navigate to web-app to connect wallet
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
// Mint ETH and buy KRK programmatically
observations.push('Buying KRK via on-chain swap...');
await mintEth(page, STACK_RPC_URL, ACCOUNT_ADDRESS, '10');
try {
await buyKrk(page, '1', STACK_RPC_URL, ACCOUNT_PRIVATE_KEY);
observations.push('✓ Successfully acquired KRK via swap');
} catch (error: any) {
observations.push(`✗ KRK purchase failed: ${error.message}`);
feedback.overall.friction.push('Cannot acquire KRK through documented flow');
}
// Navigate back to landing page
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
// Check for reasons to return
observations.push('Now I have KRK... why would I come back to landing page?');
const hasStatsSection = await page.getByText(/stats|protocol health|dashboard/i).isVisible().catch(() => false);
const hasPriceInfo = await page.locator('text=/price|\\$[0-9]/i').isVisible().catch(() => false);
const hasAPY = await page.locator('text=/APY|%/i').isVisible().catch(() => false);
if (hasStatsSection || hasPriceInfo || hasAPY) {
observations.push('✓ Landing page has stats/info - gives me reason to check back');
} else {
observations.push('✗ No compelling reason to return to landing page - just a static ad');
feedback.overall.friction.push('Landing page offers no ongoing value for holders');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'tyler-a');
const screenshotPath = join(screenshotDir, `return-check-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'return-value', observations, screenshotPath);
// Tyler's overall assessment
const wouldReturn = hasStatsSection || hasPriceInfo || hasAPY;
feedback.overall.wouldBuy = observations.some(o => o.includes('✓ Successfully acquired KRK'));
feedback.overall.wouldReturn = wouldReturn;
if (!wouldReturn) {
feedback.overall.friction.push('Landing page is one-time conversion, no repeat visit value');
}
} finally {
await context.close();
writePersonaFeedback(feedback);
}
});
});
test.describe.serial('Alex - Newcomer ("what even is this?")', () => {
let feedback: PersonaFeedback;
test.beforeAll(() => {
feedback = createPersonaFeedback('alex', 'A', 'passive-holder');
});
test('Alex tries to understand the landing page', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
observations.push('Reading the page... trying to understand what this protocol does');
// Check for explanatory content
const hasExplainer = await page.getByText(/how it works|what is|getting started/i).isVisible().catch(() => false);
if (hasExplainer) {
observations.push('✓ Found "How it works" or explainer section - helpful!');
} else {
observations.push('✗ No clear explainer - I\'m lost and don\'t know what this is');
feedback.overall.friction.push('No beginner-friendly explanation on landing page');
}
// Jargon overload check
const pageText = await page.textContent('body') || '';
const complexTerms = ['harberger', 'vwap', 'amm', 'liquidity pool', 'tokenomics', 'leverage', 'tax rate'];
const foundTerms = complexTerms.filter(term => pageText.toLowerCase().includes(term));
if (foundTerms.length > 3) {
observations.push(`⚠ Jargon overload (${foundTerms.length} complex terms): ${foundTerms.join(', ')}`);
observations.push('As a newcomer, this is intimidating and confusing');
feedback.overall.friction.push('Too much unexplained crypto jargon');
} else {
observations.push('✓ Language is relatively accessible');
}
// Check for Get KRK button clarity
const getKrkButton = await page.getByRole('button', { name: /get.*krk/i }).isVisible().catch(() => false);
if (getKrkButton) {
observations.push('✓ "Get KRK" button is clear - I understand that\'s the next step');
} else {
observations.push('✗ Not sure how to start or what to do first');
}
// Trust signals
const hasTrustSignals = await page.getByText(/audit|secure|safe|verified/i).isVisible().catch(() => false);
if (hasTrustSignals) {
observations.push('✓ Trust signals present (audit/secure) - makes me feel safer');
} else {
observations.push('⚠ No visible security/audit info - how do I know this is safe?');
feedback.overall.friction.push('Lack of trust signals for newcomers');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'alex-a');
mkdirSync(screenshotDir, { recursive: true });
const screenshotPath = join(screenshotDir, `landing-confusion-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'landing-page', observations, screenshotPath);
} finally {
await context.close();
}
});
test('Alex explores Get KRK page and looks for guidance', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1_000);
// Try to click Get KRK
const getKrkButton = page.getByRole('button', { name: /get.*krk/i }).first();
const buttonVisible = await getKrkButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (buttonVisible) {
await getKrkButton.click();
await page.waitForTimeout(2_000);
observations.push('Clicked "Get KRK" - now what?');
// Look for step-by-step instructions
const hasInstructions = await page.getByText(/step|how to|tutorial|guide/i).isVisible().catch(() => false);
if (hasInstructions) {
observations.push('✓ Found step-by-step instructions - very helpful for newcomer');
} else {
observations.push('✗ No clear instructions on how to proceed');
feedback.overall.friction.push('Get KRK page lacks step-by-step guide');
}
// Check for Uniswap link explanation
const uniswapLink = await page.locator('a[href*="uniswap"]').first().isVisible().catch(() => false);
if (uniswapLink) {
// Check if there's explanatory text near the link
const hasContext = await page.getByText(/swap|exchange|buy on uniswap/i).isVisible().catch(() => false);
if (hasContext) {
observations.push('✓ Uniswap link has context/explanation');
} else {
observations.push('⚠ Uniswap link present but no explanation - what is Uniswap?');
feedback.overall.friction.push('No explanation of external links (Uniswap)');
}
} else {
observations.push('✗ No Uniswap link found - how do I get KRK?');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'alex-a');
const screenshotPath = join(screenshotDir, `get-krk-page-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'get-krk', observations, screenshotPath);
} else {
observations.push('✗ Could not find Get KRK button');
addFeedbackStep(feedback, 'get-krk', observations);
}
} finally {
await context.close();
}
});
test('Alex simulates getting KRK and evaluates next steps', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
// Simulate getting KRK
observations.push('Pretending I figured out Uniswap and bought KRK...');
await mintEth(page, STACK_RPC_URL, ACCOUNT_ADDRESS, '10');
try {
await buyKrk(page, '1', STACK_RPC_URL, ACCOUNT_PRIVATE_KEY);
observations.push('✓ Somehow managed to get KRK');
} catch (error: any) {
observations.push(`✗ Failed to get KRK: ${error.message}`);
}
// Navigate back to landing
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
observations.push('Okay, I have KRK now... what should I do with it?');
// Look for holder guidance
const hasHolderInfo = await page.getByText(/hold|stake|earn|now what/i).isVisible().catch(() => false);
if (hasHolderInfo) {
observations.push('✓ Found guidance for what to do after getting KRK');
} else {
observations.push('✗ No clear next steps - just have tokens sitting in wallet');
feedback.overall.friction.push('No guidance for new holders on what to do next');
}
// Check for ongoing value
const hasReasonToReturn = await page.getByText(/dashboard|stats|price|track/i).isVisible().catch(() => false);
if (hasReasonToReturn) {
observations.push('✓ Landing page has info worth checking regularly');
} else {
observations.push('✗ No reason to come back to landing page');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'alex-a');
const screenshotPath = join(screenshotDir, `after-purchase-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'post-purchase', observations, screenshotPath);
// Alex's verdict
const understandsValueProp = observations.some(o => o.includes('✓ Found "How it works"'));
const knowsNextSteps = hasHolderInfo;
feedback.overall.wouldBuy = understandsValueProp && observations.some(o => o.includes('✓ Somehow managed to get KRK'));
feedback.overall.wouldReturn = hasReasonToReturn;
if (!understandsValueProp) {
feedback.overall.friction.push('Value proposition unclear to crypto newcomers');
}
if (!knowsNextSteps) {
feedback.overall.friction.push('Post-purchase journey undefined');
}
} finally {
await context.close();
writePersonaFeedback(feedback);
}
});
});
test.describe.serial('Sarah - Yield Farmer ("is this worth my time?")', () => {
let feedback: PersonaFeedback;
test.beforeAll(() => {
feedback = createPersonaFeedback('sarah', 'A', 'passive-holder');
});
test('Sarah analyzes landing page metrics and credibility', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
observations.push('Scanning for key metrics: APY, TVL, risk factors...');
// Check for APY/yield info
const hasAPY = await page.locator('text=/\\d+%|APY|yield/i').isVisible().catch(() => false);
if (hasAPY) {
observations.push('✓ APY or yield percentage visible - good, I can compare to other protocols');
} else {
observations.push('✗ No clear APY shown - can\'t evaluate if this is competitive');
feedback.overall.friction.push('No yield/APY displayed on landing page');
}
// Check for TVL
const hasTVL = await page.locator('text=/TVL|total value locked|\\$[0-9]+[kmb]/i').isVisible().catch(() => false);
if (hasTVL) {
observations.push('✓ TVL visible - helps me assess protocol size and safety');
} else {
observations.push('⚠ No TVL shown - harder to gauge protocol maturity');
}
// Protocol Health section
const hasProtocolHealth = await page.getByText(/protocol health|health|status/i).isVisible().catch(() => false);
if (hasProtocolHealth) {
observations.push('✓ Protocol Health section present - shows transparency and confidence');
} else {
observations.push('⚠ No protocol health metrics - how do I assess risk?');
feedback.overall.friction.push('Missing protocol health/risk indicators');
}
// Audit info
const hasAudit = await page.getByText(/audit|audited|security/i).isVisible().catch(() => false);
if (hasAudit) {
observations.push('✓ Audit information visible - critical for serious yield farmers');
} else {
observations.push('✗ No audit badge or security info - major red flag');
feedback.overall.friction.push('No visible audit/security credentials');
}
// Smart contract addresses
const hasContracts = await page.locator('text=/0x[a-fA-F0-9]{40}|contract address/i').isVisible().catch(() => false);
if (hasContracts) {
observations.push('✓ Contract addresses visible - I can verify on Etherscan');
} else {
observations.push('⚠ No contract addresses - want to verify before committing capital');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'sarah-a');
mkdirSync(screenshotDir, { recursive: true });
const screenshotPath = join(screenshotDir, `landing-metrics-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'landing-page', observations, screenshotPath);
} finally {
await context.close();
}
});
test('Sarah evaluates Get KRK flow efficiency', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1_000);
const getKrkButton = page.getByRole('button', { name: /get.*krk/i }).first();
const buttonVisible = await getKrkButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (buttonVisible) {
await getKrkButton.click();
await page.waitForTimeout(2_000);
observations.push('Evaluating acquisition flow - time is money');
// Check for direct swap vs external redirect
const currentUrl = page.url();
const hasDirectSwap = await page.locator('input[placeholder*="amount" i]').isVisible({ timeout: 3_000 }).catch(() => false);
if (hasDirectSwap) {
observations.push('✓ Direct swap interface - efficient, no external redirects');
} else {
observations.push('⚠ Redirects to external swap - adds friction and gas costs');
}
// Uniswap link check
const uniswapLink = await page.locator(`a[href*="uniswap"][href*="${KRK_ADDRESS}"]`).isVisible().catch(() => false);
if (uniswapLink) {
observations.push('✓ Uniswap link with correct token address - can verify liquidity');
} else {
observations.push('✗ No Uniswap link or wrong address - can\'t verify DEX liquidity');
feedback.overall.friction.push('Cannot verify DEX liquidity before buying');
}
// Price impact warning
const hasPriceImpact = await page.getByText(/price impact|slippage/i).isVisible().catch(() => false);
if (hasPriceImpact) {
observations.push('✓ Price impact/slippage shown - good UX for larger trades');
} else {
observations.push('⚠ No price impact warning - could be surprised by slippage');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'sarah-a');
const screenshotPath = join(screenshotDir, `get-krk-flow-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'get-krk', observations, screenshotPath);
} else {
observations.push('✗ Get KRK button not found');
addFeedbackStep(feedback, 'get-krk', observations);
}
} finally {
await context.close();
}
});
test('Sarah checks for holder value and monitoring tools', async ({ browser }) => {
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,
rpcUrl: STACK_RPC_URL,
});
const page = await context.newPage();
const observations: string[] = [];
try {
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
// Acquire KRK
observations.push('Acquiring KRK to evaluate holder experience...');
await mintEth(page, STACK_RPC_URL, ACCOUNT_ADDRESS, '10');
try {
await buyKrk(page, '2', STACK_RPC_URL, ACCOUNT_PRIVATE_KEY);
observations.push('✓ KRK acquired');
} catch (error: any) {
observations.push(`✗ Acquisition failed: ${error.message}`);
feedback.overall.friction.push('Programmatic acquisition flow broken');
}
// Return to landing page
await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2_000);
observations.push('Now holding KRK - what ongoing value does landing page provide?');
// Real-time stats
const hasRealtimeStats = await page.locator('text=/live|24h|volume|price/i').isVisible().catch(() => false);
if (hasRealtimeStats) {
observations.push('✓ Real-time stats visible - makes landing page a monitoring dashboard');
} else {
observations.push('✗ No real-time data - no reason to return to landing page');
feedback.overall.friction.push('Landing page provides no ongoing value for holders');
}
// Protocol health tracking
const hasHealthMetrics = await page.getByText(/protocol health|system status|health score/i).isVisible().catch(() => false);
if (hasHealthMetrics) {
observations.push('✓ Protocol health tracking - helps me monitor risk');
} else {
observations.push('⚠ No protocol health dashboard - can\'t monitor protocol risk');
}
// Links to analytics
const hasAnalytics = await page.locator('a[href*="dune"][href*="dexscreener"]').or(page.getByText(/analytics|charts/i)).isVisible().catch(() => false);
if (hasAnalytics) {
observations.push('✓ Links to analytics platforms - good for research');
} else {
observations.push('⚠ No links to Dune/DexScreener - harder to do deep analysis');
}
// Screenshot
const screenshotDir = join('test-results', 'usertest', 'sarah-a');
const screenshotPath = join(screenshotDir, `holder-dashboard-${Date.now()}.png`);
await page.screenshot({ path: screenshotPath, fullPage: true });
addFeedbackStep(feedback, 'holder-experience', observations, screenshotPath);
// Sarah's ROI assessment
const hasCompetitiveAPY = observations.some(o => o.includes('✓ APY or yield percentage visible'));
const hasMonitoringTools = hasRealtimeStats || hasHealthMetrics;
const lowFriction = feedback.overall.friction.length < 3;
feedback.overall.wouldBuy = hasCompetitiveAPY && lowFriction;
feedback.overall.wouldReturn = hasMonitoringTools;
if (!hasMonitoringTools) {
feedback.overall.friction.push('Insufficient monitoring/analytics tools for active yield farmers');
}
observations.push(`Sarah's verdict: ${feedback.overall.wouldBuy ? 'Worth allocating capital' : 'Not competitive enough'}`);
observations.push(`Would return: ${feedback.overall.wouldReturn ? 'Yes, for monitoring' : 'No, one-time interaction only'}`);
} finally {
await context.close();
writePersonaFeedback(feedback);
}
});
});
});