fodda-mcp 1.38.0 → 1.46.97
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -91
- package/dist/agents/fodda-researcher/skills.d.ts +1 -1
- package/dist/agents/fodda-researcher/skills.d.ts.map +1 -1
- package/dist/agents/fodda-researcher/skills.js +1 -1
- package/dist/brandTemplate.d.ts +1 -1
- package/dist/brandTemplate.d.ts.map +1 -1
- package/dist/brandTemplate.js +278 -83
- package/dist/brandTemplate.js.map +1 -1
- package/dist/catalogCache.d.ts +39 -0
- package/dist/catalogCache.d.ts.map +1 -1
- package/dist/catalogCache.js +293 -22
- package/dist/catalogCache.js.map +1 -1
- package/dist/coverageRelevance.d.ts +251 -0
- package/dist/coverageRelevance.d.ts.map +1 -0
- package/dist/coverageRelevance.js +1857 -0
- package/dist/coverageRelevance.js.map +1 -0
- package/dist/editorialFill.d.ts +5 -0
- package/dist/editorialFill.d.ts.map +1 -1
- package/dist/editorialFill.js +1 -1
- package/dist/editorialFill.js.map +1 -1
- package/dist/enrichment.d.ts +26 -0
- package/dist/enrichment.d.ts.map +1 -1
- package/dist/enrichment.js +442 -29
- package/dist/enrichment.js.map +1 -1
- package/dist/errorHandling.d.ts +35 -7
- package/dist/errorHandling.d.ts.map +1 -1
- package/dist/errorHandling.js +225 -155
- package/dist/errorHandling.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -106
- package/dist/index.js.map +1 -1
- package/dist/pricingCache.d.ts.map +1 -1
- package/dist/pricingCache.js +55 -3
- package/dist/pricingCache.js.map +1 -1
- package/dist/queryCache.d.ts.map +1 -1
- package/dist/queryCache.js +2 -0
- package/dist/queryCache.js.map +1 -1
- package/dist/reportBriefing.d.ts +89 -0
- package/dist/reportBriefing.d.ts.map +1 -0
- package/dist/reportBriefing.js +662 -0
- package/dist/reportBriefing.js.map +1 -0
- package/dist/searchTemplate.d.ts.map +1 -1
- package/dist/searchTemplate.js +65 -53
- package/dist/searchTemplate.js.map +1 -1
- package/dist/services/graphRouter.d.ts +7 -0
- package/dist/services/graphRouter.d.ts.map +1 -0
- package/dist/services/graphRouter.js +7 -0
- package/dist/services/graphRouter.js.map +1 -0
- package/dist/services/notifications.d.ts +2 -2
- package/dist/services/notifications.js +3 -3
- package/dist/services/notifications.js.map +1 -1
- package/dist/sessionTracker.d.ts +42 -3
- package/dist/sessionTracker.d.ts.map +1 -1
- package/dist/sessionTracker.js +261 -14
- package/dist/sessionTracker.js.map +1 -1
- package/dist/skillClient.d.ts.map +1 -1
- package/dist/skillClient.js +11 -6
- package/dist/skillClient.js.map +1 -1
- package/dist/svgVisuals.d.ts +17 -19
- package/dist/svgVisuals.d.ts.map +1 -1
- package/dist/svgVisuals.js +189 -233
- package/dist/svgVisuals.js.map +1 -1
- package/dist/systemPrompt.d.ts +7 -2
- package/dist/systemPrompt.d.ts.map +1 -1
- package/dist/systemPrompt.js +153 -81
- package/dist/systemPrompt.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +15 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/test_adjacent_trends_polymorphic.d.ts +2 -0
- package/dist/test_adjacent_trends_polymorphic.d.ts.map +1 -0
- package/dist/test_adjacent_trends_polymorphic.js +154 -0
- package/dist/test_adjacent_trends_polymorphic.js.map +1 -0
- package/dist/test_book_a_call_live.d.ts +2 -0
- package/dist/test_book_a_call_live.d.ts.map +1 -0
- package/dist/test_book_a_call_live.js +161 -0
- package/dist/test_book_a_call_live.js.map +1 -0
- package/dist/test_brand_intelligence_endpoint.js +2 -2
- package/dist/test_byo_mcp_submission_clarity.d.ts +2 -0
- package/dist/test_byo_mcp_submission_clarity.d.ts.map +1 -0
- package/dist/test_byo_mcp_submission_clarity.js +142 -0
- package/dist/test_byo_mcp_submission_clarity.js.map +1 -0
- package/dist/test_capabilities_guidance.d.ts +2 -0
- package/dist/test_capabilities_guidance.d.ts.map +1 -0
- package/dist/test_capabilities_guidance.js +372 -0
- package/dist/test_capabilities_guidance.js.map +1 -0
- package/dist/test_client_provenance.d.ts +2 -0
- package/dist/test_client_provenance.d.ts.map +1 -0
- package/dist/test_client_provenance.js +77 -0
- package/dist/test_client_provenance.js.map +1 -0
- package/dist/test_connection_surface_cleanup.d.ts +2 -0
- package/dist/test_connection_surface_cleanup.d.ts.map +1 -0
- package/dist/test_connection_surface_cleanup.js +203 -0
- package/dist/test_connection_surface_cleanup.js.map +1 -0
- package/dist/test_consult_routing.d.ts +2 -0
- package/dist/test_consult_routing.d.ts.map +1 -0
- package/dist/test_consult_routing.js +86 -0
- package/dist/test_consult_routing.js.map +1 -0
- package/dist/test_consult_split.d.ts +2 -0
- package/dist/test_consult_split.d.ts.map +1 -0
- package/dist/test_consult_split.js +30 -0
- package/dist/test_consult_split.js.map +1 -0
- package/dist/test_coverage_relevance.d.ts +12 -0
- package/dist/test_coverage_relevance.d.ts.map +1 -0
- package/dist/test_coverage_relevance.js +162 -0
- package/dist/test_coverage_relevance.js.map +1 -0
- package/dist/test_coverage_replay.d.ts +2 -0
- package/dist/test_coverage_replay.d.ts.map +1 -0
- package/dist/test_coverage_replay.js +170 -0
- package/dist/test_coverage_replay.js.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.d.ts +2 -0
- package/dist/test_dcr_and_legacy_deprecation.d.ts.map +1 -0
- package/dist/test_dcr_and_legacy_deprecation.js +143 -0
- package/dist/test_dcr_and_legacy_deprecation.js.map +1 -0
- package/dist/test_demand_signal_logging.d.ts +2 -0
- package/dist/test_demand_signal_logging.d.ts.map +1 -0
- package/dist/test_demand_signal_logging.js +92 -0
- package/dist/test_demand_signal_logging.js.map +1 -0
- package/dist/test_earnings_activity_scope.d.ts +5 -0
- package/dist/test_earnings_activity_scope.d.ts.map +1 -0
- package/dist/test_earnings_activity_scope.js +101 -0
- package/dist/test_earnings_activity_scope.js.map +1 -0
- package/dist/test_earnings_divergence_live.d.ts +5 -0
- package/dist/test_earnings_divergence_live.d.ts.map +1 -0
- package/dist/test_earnings_divergence_live.js +135 -0
- package/dist/test_earnings_divergence_live.js.map +1 -0
- package/dist/test_earnings_hygiene_and_binding.d.ts +5 -0
- package/dist/test_earnings_hygiene_and_binding.d.ts.map +1 -0
- package/dist/test_earnings_hygiene_and_binding.js +104 -0
- package/dist/test_earnings_hygiene_and_binding.js.map +1 -0
- package/dist/test_exec_quotes_render.d.ts +2 -0
- package/dist/test_exec_quotes_render.d.ts.map +1 -0
- package/dist/test_exec_quotes_render.js +86 -0
- package/dist/test_exec_quotes_render.js.map +1 -0
- package/dist/test_expert_intro.d.ts +2 -0
- package/dist/test_expert_intro.d.ts.map +1 -0
- package/dist/test_expert_intro.js +57 -0
- package/dist/test_expert_intro.js.map +1 -0
- package/dist/test_expert_layer.d.ts +11 -0
- package/dist/test_expert_layer.d.ts.map +1 -0
- package/dist/test_expert_layer.js +356 -0
- package/dist/test_expert_layer.js.map +1 -0
- package/dist/test_financial_snapshot_render.d.ts +2 -0
- package/dist/test_financial_snapshot_render.d.ts.map +1 -0
- package/dist/test_financial_snapshot_render.js +167 -0
- package/dist/test_financial_snapshot_render.js.map +1 -0
- package/dist/test_gap_alert.d.ts +12 -0
- package/dist/test_gap_alert.d.ts.map +1 -0
- package/dist/test_gap_alert.js +51 -0
- package/dist/test_gap_alert.js.map +1 -0
- package/dist/test_grok_offering_surfaces.d.ts +2 -0
- package/dist/test_grok_offering_surfaces.d.ts.map +1 -0
- package/dist/test_grok_offering_surfaces.js +155 -0
- package/dist/test_grok_offering_surfaces.js.map +1 -0
- package/dist/test_human_agent_live.d.ts +2 -0
- package/dist/test_human_agent_live.d.ts.map +1 -0
- package/dist/test_human_agent_live.js +48 -0
- package/dist/test_human_agent_live.js.map +1 -0
- package/dist/test_identity_gap.d.ts +2 -0
- package/dist/test_identity_gap.d.ts.map +1 -0
- package/dist/test_identity_gap.js +171 -0
- package/dist/test_identity_gap.js.map +1 -0
- package/dist/test_intelligence_and_verifier.d.ts +17 -0
- package/dist/test_intelligence_and_verifier.d.ts.map +1 -0
- package/dist/test_intelligence_and_verifier.js +162 -0
- package/dist/test_intelligence_and_verifier.js.map +1 -0
- package/dist/test_latency_mcp_brief5.d.ts +12 -0
- package/dist/test_latency_mcp_brief5.d.ts.map +1 -0
- package/dist/test_latency_mcp_brief5.js +200 -0
- package/dist/test_latency_mcp_brief5.js.map +1 -0
- package/dist/test_limit_and_credit_card_capture.d.ts +21 -0
- package/dist/test_limit_and_credit_card_capture.d.ts.map +1 -0
- package/dist/test_limit_and_credit_card_capture.js +216 -0
- package/dist/test_limit_and_credit_card_capture.js.map +1 -0
- package/dist/test_list_graphs_topics.js +1 -1
- package/dist/test_list_graphs_topics.js.map +1 -1
- package/dist/test_live_deployed_mcp_search.d.ts +2 -0
- package/dist/test_live_deployed_mcp_search.d.ts.map +1 -0
- package/dist/test_live_deployed_mcp_search.js +85 -0
- package/dist/test_live_deployed_mcp_search.js.map +1 -0
- package/dist/test_live_deployed_split.d.ts +2 -0
- package/dist/test_live_deployed_split.d.ts.map +1 -0
- package/dist/test_live_deployed_split.js +72 -0
- package/dist/test_live_deployed_split.js.map +1 -0
- package/dist/test_live_endpoint_runner.js +2 -2
- package/dist/test_live_endpoint_runner.js.map +1 -1
- package/dist/test_live_mcp.js +1 -1
- package/dist/test_live_mcp.js.map +1 -1
- package/dist/test_live_neil_widget.d.ts +2 -0
- package/dist/test_live_neil_widget.d.ts.map +1 -0
- package/dist/test_live_neil_widget.js +84 -0
- package/dist/test_live_neil_widget.js.map +1 -0
- package/dist/test_longpoll_status.d.ts +2 -0
- package/dist/test_longpoll_status.d.ts.map +1 -0
- package/dist/test_longpoll_status.js +117 -0
- package/dist/test_longpoll_status.js.map +1 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts +2 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.d.ts.map +1 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.js +130 -0
- package/dist/test_mcp_find_expert_and_vocabulary_routing.js.map +1 -0
- package/dist/test_next_moves.d.ts +2 -0
- package/dist/test_next_moves.d.ts.map +1 -0
- package/dist/test_next_moves.js +714 -0
- package/dist/test_next_moves.js.map +1 -0
- package/dist/test_next_moves_transcripts.d.ts +2 -0
- package/dist/test_next_moves_transcripts.d.ts.map +1 -0
- package/dist/test_next_moves_transcripts.js +495 -0
- package/dist/test_next_moves_transcripts.js.map +1 -0
- package/dist/test_on_request_caller_identity.d.ts +13 -0
- package/dist/test_on_request_caller_identity.d.ts.map +1 -0
- package/dist/test_on_request_caller_identity.js +131 -0
- package/dist/test_on_request_caller_identity.js.map +1 -0
- package/dist/test_on_request_intercept.d.ts +10 -0
- package/dist/test_on_request_intercept.d.ts.map +1 -0
- package/dist/test_on_request_intercept.js +95 -0
- package/dist/test_on_request_intercept.js.map +1 -0
- package/dist/test_onboarding_consent_and_errors.d.ts +2 -0
- package/dist/test_onboarding_consent_and_errors.d.ts.map +1 -0
- package/dist/test_onboarding_consent_and_errors.js +110 -0
- package/dist/test_onboarding_consent_and_errors.js.map +1 -0
- package/dist/test_onboarding_live_scenarios.d.ts +2 -0
- package/dist/test_onboarding_live_scenarios.d.ts.map +1 -0
- package/dist/test_onboarding_live_scenarios.js +189 -0
- package/dist/test_onboarding_live_scenarios.js.map +1 -0
- package/dist/test_referral_live.d.ts +2 -0
- package/dist/test_referral_live.d.ts.map +1 -0
- package/dist/test_referral_live.js +33 -0
- package/dist/test_referral_live.js.map +1 -0
- package/dist/test_referral_piers.d.ts +2 -0
- package/dist/test_referral_piers.d.ts.map +1 -0
- package/dist/test_referral_piers.js +33 -0
- package/dist/test_referral_piers.js.map +1 -0
- package/dist/test_report_editorial_briefing.d.ts +21 -0
- package/dist/test_report_editorial_briefing.d.ts.map +1 -0
- package/dist/test_report_editorial_briefing.js +356 -0
- package/dist/test_report_editorial_briefing.js.map +1 -0
- package/dist/test_search_graph_quality.d.ts +2 -0
- package/dist/test_search_graph_quality.d.ts.map +1 -0
- package/dist/test_search_graph_quality.js +493 -0
- package/dist/test_search_graph_quality.js.map +1 -0
- package/dist/test_service_to_service_auth.d.ts +2 -0
- package/dist/test_service_to_service_auth.d.ts.map +1 -0
- package/dist/test_service_to_service_auth.js +199 -0
- package/dist/test_service_to_service_auth.js.map +1 -0
- package/dist/test_session_next_moves_telemetry.d.ts +2 -0
- package/dist/test_session_next_moves_telemetry.d.ts.map +1 -0
- package/dist/test_session_next_moves_telemetry.js +100 -0
- package/dist/test_session_next_moves_telemetry.js.map +1 -0
- package/dist/test_sources_live.d.ts +2 -0
- package/dist/test_sources_live.d.ts.map +1 -0
- package/dist/test_sources_live.js +30 -0
- package/dist/test_sources_live.js.map +1 -0
- package/dist/test_specialist_and_analyst_tiers.d.ts +2 -0
- package/dist/test_specialist_and_analyst_tiers.d.ts.map +1 -0
- package/dist/test_specialist_and_analyst_tiers.js +155 -0
- package/dist/test_specialist_and_analyst_tiers.js.map +1 -0
- package/dist/test_v2_2_visual_hardening.d.ts +5 -0
- package/dist/test_v2_2_visual_hardening.d.ts.map +1 -0
- package/dist/test_v2_2_visual_hardening.js +333 -0
- package/dist/test_v2_2_visual_hardening.js.map +1 -0
- package/dist/test_verify_deprecate_render_spec.d.ts +2 -0
- package/dist/test_verify_deprecate_render_spec.d.ts.map +1 -0
- package/dist/test_verify_deprecate_render_spec.js +217 -0
- package/dist/test_verify_deprecate_render_spec.js.map +1 -0
- package/dist/test_verify_neil_demo.d.ts +2 -0
- package/dist/test_verify_neil_demo.d.ts.map +1 -0
- package/dist/test_verify_neil_demo.js +149 -0
- package/dist/test_verify_neil_demo.js.map +1 -0
- package/dist/test_waitable_research_and_grounded_links.d.ts +2 -0
- package/dist/test_waitable_research_and_grounded_links.d.ts.map +1 -0
- package/dist/test_waitable_research_and_grounded_links.js +129 -0
- package/dist/test_waitable_research_and_grounded_links.js.map +1 -0
- package/dist/test_widget_guards.d.ts +2 -0
- package/dist/test_widget_guards.d.ts.map +1 -0
- package/dist/test_widget_guards.js +229 -0
- package/dist/test_widget_guards.js.map +1 -0
- package/dist/test_zero_retention_alerts.d.ts +14 -0
- package/dist/test_zero_retention_alerts.d.ts.map +1 -0
- package/dist/test_zero_retention_alerts.js +76 -0
- package/dist/test_zero_retention_alerts.js.map +1 -0
- package/dist/toolHandlers.d.ts +38 -1
- package/dist/toolHandlers.d.ts.map +1 -1
- package/dist/toolHandlers.js +4290 -711
- package/dist/toolHandlers.js.map +1 -1
- package/dist/tools.d.ts +22 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +29 -4
- package/dist/tools.js.map +1 -1
- package/dist/verify_tools_endpoint.js +19 -5
- package/dist/verify_tools_endpoint.js.map +1 -1
- package/dist/widgetShell.d.ts +2 -2
- package/dist/widgetShell.d.ts.map +1 -1
- package/dist/widgetShell.js +1 -1
- package/dist/widgetShell.js.map +1 -1
- package/package.json +1 -1
- package/server.json +3 -31
- package/dist/debug_tools.d.ts +0 -2
- package/dist/debug_tools.d.ts.map +0 -1
- package/dist/debug_tools.js +0 -19
- package/dist/debug_tools.js.map +0 -1
- package/dist/inspect_list_graphs.d.ts +0 -2
- package/dist/inspect_list_graphs.d.ts.map +0 -1
- package/dist/inspect_list_graphs.js +0 -25
- package/dist/inspect_list_graphs.js.map +0 -1
- package/dist/scratch_test.d.ts.map +0 -1
- package/dist/scripts/simulate_gemini.d.ts +0 -2
- package/dist/scripts/simulate_gemini.d.ts.map +0 -1
- package/dist/scripts/simulate_gemini.js +0 -100
- package/dist/scripts/simulate_gemini.js.map +0 -1
- package/dist/scripts/verify_registry.d.ts +0 -2
- package/dist/scripts/verify_registry.d.ts.map +0 -1
- package/dist/scripts/verify_registry.js +0 -77
- package/dist/scripts/verify_registry.js.map +0 -1
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -332
- package/dist/src/index.js.map +0 -1
- package/dist/src/tools.d.ts +0 -12
- package/dist/src/tools.d.ts.map +0 -1
- package/dist/src/tools.js +0 -104
- package/dist/src/tools.js.map +0 -1
- package/dist/src/types.d.ts +0 -44
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -2
- package/dist/src/types.js.map +0 -1
- package/dist/src/verify.d.ts +0 -2
- package/dist/src/verify.d.ts.map +0 -1
- package/dist/src/verify.js +0 -47
- package/dist/src/verify.js.map +0 -1
- package/dist/src/verify_hmac.d.ts +0 -2
- package/dist/src/verify_hmac.d.ts.map +0 -1
- package/dist/src/verify_hmac.js +0 -106
- package/dist/src/verify_hmac.js.map +0 -1
- package/dist/src/verify_tools_endpoint.d.ts +0 -2
- package/dist/src/verify_tools_endpoint.d.ts.map +0 -1
- package/dist/src/verify_tools_endpoint.js +0 -40
- package/dist/src/verify_tools_endpoint.js.map +0 -1
- package/dist/test-waverunner-response.d.ts +0 -2
- package/dist/test-waverunner-response.d.ts.map +0 -1
- package/dist/test-waverunner-response.js +0 -20
- package/dist/test-waverunner-response.js.map +0 -1
- package/dist/test_coverage_r2.d.ts +0 -2
- package/dist/test_coverage_r2.d.ts.map +0 -1
- package/dist/test_coverage_scratch.d.ts +0 -2
- package/dist/test_coverage_scratch.d.ts.map +0 -1
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { generateNextMoves, generateConsultNextMoves, renderConsultClosingEnvelope, clearSuggestCacheForTesting } from './coverageRelevance.js';
|
|
3
|
+
import { setCachedCatalogForTesting } from './catalogCache.js';
|
|
4
|
+
console.log('--- Running Next Moves Unit Tests ---');
|
|
5
|
+
// Mock graphs
|
|
6
|
+
const mockGraphs = [
|
|
7
|
+
{
|
|
8
|
+
graph_id: 'retail',
|
|
9
|
+
name: 'Retail Strategy & Innovation',
|
|
10
|
+
graph_type: 'domain',
|
|
11
|
+
status: 'live',
|
|
12
|
+
topics: ['retail'],
|
|
13
|
+
trend_count: 150,
|
|
14
|
+
evidence_count: 600,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
graph_id: 'ben-dietz-sic',
|
|
18
|
+
name: '[SIC] Weekly — Cultural Strategy',
|
|
19
|
+
graph_type: 'expert',
|
|
20
|
+
status: 'live',
|
|
21
|
+
topics: ['culture'],
|
|
22
|
+
trend_count: 80,
|
|
23
|
+
evidence_count: 300,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
graph_id: 'beauty',
|
|
27
|
+
name: 'Beauty & Wellness',
|
|
28
|
+
graph_type: 'domain',
|
|
29
|
+
status: 'live',
|
|
30
|
+
topics: ['beauty'],
|
|
31
|
+
trend_count: 120,
|
|
32
|
+
evidence_count: 450,
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
// Mock analysts
|
|
36
|
+
const mockAnalysts = [
|
|
37
|
+
{
|
|
38
|
+
analyst_id: 'ben-dietz-sic',
|
|
39
|
+
name: 'Ben Dietz',
|
|
40
|
+
status: 'Active',
|
|
41
|
+
topics: ['culture', 'marketing', 'trends'],
|
|
42
|
+
description: 'Cultural strategy and youth marketing',
|
|
43
|
+
graph_type: 'expert',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
analyst_id: 'retail-lead',
|
|
47
|
+
name: 'Retail Strategy Lead',
|
|
48
|
+
status: 'Active',
|
|
49
|
+
topics: ['retail', 'commerce', 'omnichannel'],
|
|
50
|
+
description: 'Omnichannel retail innovation',
|
|
51
|
+
graph_type: 'expert',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
analyst_id: 'inactive-expert',
|
|
55
|
+
name: 'Inactive Guy',
|
|
56
|
+
status: 'Paused',
|
|
57
|
+
topics: ['retail'],
|
|
58
|
+
description: 'Old retail',
|
|
59
|
+
graph_type: 'expert',
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
// Initialize catalog cache for testing
|
|
63
|
+
setCachedCatalogForTesting({ version: '1.0', generated_at: new Date().toISOString(), graph_count: mockGraphs.length, graphs: mockGraphs }, mockAnalysts);
|
|
64
|
+
async function runTests() {
|
|
65
|
+
// Test 1: Full coverage with more signals remaining
|
|
66
|
+
{
|
|
67
|
+
const rows = [
|
|
68
|
+
{ title: 'AI in store operations', brandNames: ['Nike', 'Adidas'], graphId: 'retail', theme: 'AI in retail' },
|
|
69
|
+
{ title: 'Smart checkout systems', brands: ['Amazon'], graphId: 'retail', theme: 'AI in retail' },
|
|
70
|
+
];
|
|
71
|
+
const nextMoves = await generateNextMoves(rows, 'AI in retail stores', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, { total: 10, onTopicTotal: 10, knownBrand: 'Nike' });
|
|
72
|
+
assert.ok(nextMoves, 'nextMoves should be defined');
|
|
73
|
+
assert.strictEqual(nextMoves.presentation, 'internal');
|
|
74
|
+
assert.strictEqual(nextMoves.scope_prompt, true);
|
|
75
|
+
assert.strictEqual(nextMoves.scope, 'Want this cut to Nike specifically?');
|
|
76
|
+
assert.strictEqual(nextMoves.known_brand, 'Nike');
|
|
77
|
+
assert.strictEqual(nextMoves.thread?.kind, 'more_in_graph');
|
|
78
|
+
assert.strictEqual(nextMoves.thread?.graph_id, 'retail');
|
|
79
|
+
assert.strictEqual(nextMoves.thread?.remaining_count, 8);
|
|
80
|
+
assert.ok(nextMoves.specific?.brands?.includes('Nike'));
|
|
81
|
+
assert.ok(nextMoves.specific?.brands?.includes('Adidas'));
|
|
82
|
+
console.log('✅ Test 1 Passed: Full coverage with remaining signals');
|
|
83
|
+
}
|
|
84
|
+
// Test 2: Thin coverage with adjacent room
|
|
85
|
+
{
|
|
86
|
+
const rows = [
|
|
87
|
+
{ title: 'Underground fashion drops', graphId: 'ben-dietz-sic' }
|
|
88
|
+
];
|
|
89
|
+
const nextMoves = await generateNextMoves(rows, 'underground streetwear subcultures', ['retail'], 'thin', undefined, undefined, mockGraphs, mockAnalysts);
|
|
90
|
+
assert.ok(nextMoves, 'nextMoves should be defined');
|
|
91
|
+
assert.ok(nextMoves.thread?.kind === 'adjacent_room' || nextMoves.thread?.kind === 'honest_thin');
|
|
92
|
+
assert.ok(nextMoves.specific?.expert?.display_name, 'Should recommend a relevant active expert');
|
|
93
|
+
assert.notStrictEqual(nextMoves.specific?.expert?.analyst_id, 'inactive-expert', 'Never recommend inactive expert');
|
|
94
|
+
console.log('✅ Test 2 Passed: Thin coverage with adjacent room & expert match');
|
|
95
|
+
}
|
|
96
|
+
// Test 3: Expert consult exclusion
|
|
97
|
+
{
|
|
98
|
+
const rows = [
|
|
99
|
+
{ title: 'Streetwear culture shift', brands: ['Supreme', 'Stussy'], graphId: 'ben-dietz-sic' }
|
|
100
|
+
];
|
|
101
|
+
const nextMoves = await generateNextMoves(rows, 'streetwear trends', ['ben-dietz-sic'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, { currentAnalystId: 'ben-dietz-sic' });
|
|
102
|
+
assert.ok(nextMoves, 'nextMoves should be defined');
|
|
103
|
+
if (nextMoves.specific?.expert) {
|
|
104
|
+
assert.notStrictEqual(nextMoves.specific.expert.analyst_id, 'ben-dietz-sic', 'Should not recommend the analyst currently being consulted');
|
|
105
|
+
}
|
|
106
|
+
assert.deepStrictEqual(nextMoves.specific?.brands, ['Supreme', 'Stussy']);
|
|
107
|
+
console.log('✅ Test 3 Passed: Expert consult does not recommend self');
|
|
108
|
+
}
|
|
109
|
+
// Test 4: OK coverage with 0 remainder and all relevant graphs searched -> thread should be undefined
|
|
110
|
+
{
|
|
111
|
+
const rows = [
|
|
112
|
+
{ title: 'Trend 1', graphId: 'retail' },
|
|
113
|
+
{ title: 'Trend 2', graphId: 'ben-dietz-sic' },
|
|
114
|
+
{ title: 'Trend 3', graphId: 'beauty' },
|
|
115
|
+
];
|
|
116
|
+
const nextMoves = await generateNextMoves(rows, 'retail strategy and cultural trends', ['retail', 'ben-dietz-sic', 'beauty', 'retail-lead'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, { total: 3, onTopicTotal: 3 });
|
|
117
|
+
assert.ok(nextMoves, 'nextMoves should be defined');
|
|
118
|
+
assert.strictEqual(nextMoves.thread, undefined, 'Thread should be dropped when ok coverage has 0 remainder and no unsearched room');
|
|
119
|
+
assert.strictEqual(nextMoves.scope, "If you tell me the brand or brief you're working on, I'll cut this to that.");
|
|
120
|
+
console.log('✅ Test 4 Passed: OK coverage with 0 remainder drops thread line');
|
|
121
|
+
}
|
|
122
|
+
// Test 5: Statistics source populated on market/retail queries
|
|
123
|
+
{
|
|
124
|
+
const rows = [{ title: 'Retail spending surge', graphId: 'retail' }];
|
|
125
|
+
const nextMoves = await generateNextMoves(rows, 'retail consumer spending market trends', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts);
|
|
126
|
+
assert.ok(nextMoves.specific?.statistics_source, 'Should recommend a statistics source for retail spending queries');
|
|
127
|
+
assert.ok(nextMoves.specific.statistics_source.includes('Census'), 'Should recommend Census for retail spending');
|
|
128
|
+
console.log('✅ Test 5 Passed: Statistics source populated for market query');
|
|
129
|
+
}
|
|
130
|
+
// ── Consult-Specific Next Moves Unit Tests (Render Spec 1.3) ──
|
|
131
|
+
// Test 6: Consult with explicit expert_thread.next_angle
|
|
132
|
+
{
|
|
133
|
+
const mockResult = {
|
|
134
|
+
coverage: 'in',
|
|
135
|
+
report: 'In my cultural strategy framework, community commerce requires authentic creator alignment.',
|
|
136
|
+
sources_used: [{ title: 'Creator Collectives and Community Commerce', type: 'own_graph', graph_id: 'ben-dietz-sic' }],
|
|
137
|
+
expert_thread: {
|
|
138
|
+
on_topic_total: 8,
|
|
139
|
+
cited_count: 1,
|
|
140
|
+
uncited_themes: ['Zines & Subcultures', 'Discord Councils'],
|
|
141
|
+
brands: ['Supreme', 'Aimé Leon Dore'],
|
|
142
|
+
next_angle: 'We can explore how creator collectives differ across European luxury markets next.'
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'cultural community commerce and luxury retail', 'ben-dietz-sic', { knownBrand: 'Aimé Leon Dore' }, mockGraphs, mockAnalysts);
|
|
146
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
147
|
+
assert.strictEqual(nextMoves.consult_envelope.thread_line, 'We can explore how creator collectives differ across European luxury markets next.');
|
|
148
|
+
// Shelf must exclude Ben Dietz's own graph
|
|
149
|
+
assert.ok(nextMoves.consult_envelope.shelf_line, 'Shelf line must be defined');
|
|
150
|
+
assert.ok(!nextMoves.consult_envelope.shelf_line.includes('Ben Dietz'), 'Shelf must strictly exclude current expert');
|
|
151
|
+
assert.ok(nextMoves.consult_envelope.shelf_line.startsWith('Fodda also holds trend signals on this in'), 'Shelf must use standard platform phrasing');
|
|
152
|
+
assert.strictEqual(nextMoves.consult_envelope.scope_line, 'Want this cut to Aimé Leon Dore specifically?');
|
|
153
|
+
const rendered = renderConsultClosingEnvelope(nextMoves);
|
|
154
|
+
assert.strictEqual(rendered.lines.length, 3, 'Must render exactly 3 sentences');
|
|
155
|
+
console.log('✅ Test 6 Passed: Consult with explicit next_angle and 1.2 scope copy');
|
|
156
|
+
}
|
|
157
|
+
// Test 7: Consult fallback to uncited_themes when next_angle is absent
|
|
158
|
+
{
|
|
159
|
+
const mockResult = {
|
|
160
|
+
coverage: 'in',
|
|
161
|
+
report: 'Community commerce analysis.',
|
|
162
|
+
sources_used: [{ title: 'Creator Collectives', type: 'own_graph', graph_id: 'ben-dietz-sic' }],
|
|
163
|
+
expert_thread: {
|
|
164
|
+
on_topic_total: 5,
|
|
165
|
+
cited_count: 1,
|
|
166
|
+
uncited_themes: ['Discord Councils', 'Zines & Subcultures'],
|
|
167
|
+
brands: ['Supreme'],
|
|
168
|
+
next_angle: null
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'cultural community commerce', 'ben-dietz-sic', undefined, mockGraphs, mockAnalysts);
|
|
172
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
173
|
+
assert.strictEqual(nextMoves.consult_envelope.thread_line, 'If you want to stay on this, we can look into Discord Councils in my graph.');
|
|
174
|
+
assert.strictEqual(nextMoves.consult_envelope.scope_line, "If you tell me the brand or brief you're working on, I'll cut this to that.");
|
|
175
|
+
console.log('✅ Test 7 Passed: Consult fallback to uncited_themes with 1.2 scope copy');
|
|
176
|
+
}
|
|
177
|
+
// Test 8: Consult fallback to graph remainder when next_angle and uncited_themes are absent
|
|
178
|
+
{
|
|
179
|
+
const mockResult = {
|
|
180
|
+
coverage: 'in',
|
|
181
|
+
report: 'Community commerce analysis.',
|
|
182
|
+
sources_used: [{ title: 'Creator Collectives', type: 'own_graph', graph_id: 'ben-dietz-sic' }],
|
|
183
|
+
expert_thread: {
|
|
184
|
+
on_topic_total: 6,
|
|
185
|
+
cited_count: 1,
|
|
186
|
+
uncited_themes: [],
|
|
187
|
+
brands: [],
|
|
188
|
+
next_angle: null
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'cultural community commerce', 'ben-dietz-sic', undefined, mockGraphs, mockAnalysts);
|
|
192
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
193
|
+
assert.strictEqual(nextMoves.consult_envelope.thread_line, 'There are several more trends in my graph exploring this topic — want me to pull those?');
|
|
194
|
+
assert.strictEqual(nextMoves.consult_envelope.scope_line, "If you tell me the brand or brief you're working on, I'll cut this to that.");
|
|
195
|
+
console.log('✅ Test 8 Passed: Consult fallback to graph remainder');
|
|
196
|
+
}
|
|
197
|
+
// Test 9: Out-of-lane / decline consult with referrals
|
|
198
|
+
{
|
|
199
|
+
const mockResult = {
|
|
200
|
+
coverage: 'out',
|
|
201
|
+
report: 'This Human Agent does not cover technical retail logistics.',
|
|
202
|
+
sources_used: [],
|
|
203
|
+
referrals: [
|
|
204
|
+
{ id: 'retail-lead', name: 'Retail Strategy Lead', curator: 'Retail Strategy Lead', reason: 'omnichannel retail logistics directly', status: 'Active' }
|
|
205
|
+
],
|
|
206
|
+
expert_thread: {
|
|
207
|
+
on_topic_total: 0,
|
|
208
|
+
cited_count: 0,
|
|
209
|
+
uncited_themes: [],
|
|
210
|
+
brands: [],
|
|
211
|
+
next_angle: null
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'warehouse logistics automation', 'ben-dietz-sic', undefined, mockGraphs, mockAnalysts);
|
|
215
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
216
|
+
assert.strictEqual(nextMoves.consult_envelope.thread_line, "For inquiries on this topic, I'd recommend connecting with Retail Strategy Lead who covers omnichannel retail logistics directly.");
|
|
217
|
+
console.log('✅ Test 9 Passed: Out-of-lane consult with peer referral');
|
|
218
|
+
}
|
|
219
|
+
// Test 10: next_angle token check failure triggers fallback per §2.A.5
|
|
220
|
+
{
|
|
221
|
+
const mockResult = {
|
|
222
|
+
coverage: 'in',
|
|
223
|
+
report: 'Cultural brands analysis.',
|
|
224
|
+
sources_used: [{ title: 'Streetwear Dynamics', type: 'own_graph', graph_id: 'ben-dietz-sic' }],
|
|
225
|
+
expert_thread: {
|
|
226
|
+
on_topic_total: 5,
|
|
227
|
+
cited_count: 1,
|
|
228
|
+
uncited_themes: ['Underground Music'],
|
|
229
|
+
brands: [],
|
|
230
|
+
// Hallucinated / completely unrelated angle sharing no tokens with sources or uncited themes:
|
|
231
|
+
next_angle: 'We should look into quantum computing aerospace satellites.'
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'streetwear dynamics', 'ben-dietz-sic', undefined, mockGraphs, mockAnalysts);
|
|
235
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
236
|
+
assert.strictEqual(nextMoves.consult_envelope.thread_line, 'If you want to stay on this, we can look into Underground Music in my graph.', 'Ungrounded next_angle must fail token check and fall back to uncited_themes');
|
|
237
|
+
console.log('✅ Test 10 Passed: next_angle token check failure triggers clean fallback');
|
|
238
|
+
}
|
|
239
|
+
// Test 11: Consult with empty shelf candidate list omits sentence 2 (2 sentences rendered)
|
|
240
|
+
{
|
|
241
|
+
const mockResult = {
|
|
242
|
+
coverage: 'in',
|
|
243
|
+
report: 'Niche topic report.',
|
|
244
|
+
sources_used: [{ title: 'Niche Analysis', type: 'own_graph', graph_id: 'ben-dietz-sic' }],
|
|
245
|
+
expert_thread: {
|
|
246
|
+
on_topic_total: 3,
|
|
247
|
+
cited_count: 1,
|
|
248
|
+
uncited_themes: ['Subculture Zines'],
|
|
249
|
+
next_angle: 'We can explore subculture zines next.'
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
// Only 1 graph in catalog (the expert's own graph), so no candidate shelf graphs exist
|
|
253
|
+
const onlyOwnGraph = [mockGraphs[1]];
|
|
254
|
+
setCachedCatalogForTesting({ version: '1.0', generated_at: new Date().toISOString(), graph_count: 1, graphs: onlyOwnGraph }, mockAnalysts);
|
|
255
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'obscure query with no other graph coverage', 'ben-dietz-sic', undefined, onlyOwnGraph, mockAnalysts);
|
|
256
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
257
|
+
assert.strictEqual(nextMoves.consult_envelope.shelf_line, undefined, 'shelf_line must be omitted when no relevant graphs exist');
|
|
258
|
+
assert.strictEqual(nextMoves.shelf, undefined, 'shelf must be undefined when no candidates exist');
|
|
259
|
+
const rendered = renderConsultClosingEnvelope(nextMoves);
|
|
260
|
+
assert.strictEqual(rendered.lines.length, 2, 'Must render exactly 2 sentences when shelf is omitted');
|
|
261
|
+
assert.strictEqual(rendered.text, "We can explore subculture zines next. If you tell me the brand or brief you're working on, I'll cut this to that.");
|
|
262
|
+
console.log('✅ Test 11 Passed: Consult with empty shelf candidates cleanly omits sentence 2');
|
|
263
|
+
}
|
|
264
|
+
// Test 12: Pinned regression test against 1.46.23 envelope for search_graph("Gen Z beverage hydration trends")
|
|
265
|
+
{
|
|
266
|
+
const rows = [
|
|
267
|
+
{ title: 'Functional Hydration Beverages', brandNames: ['Liquid IV', 'Gatorade'], graphId: 'retail', score: 1.8, topics: ['beverage', 'retail'], on_topic_total: 8 },
|
|
268
|
+
{ title: 'Electrolyte Micro-Dosing', brandNames: ['Waterdrop'], graphId: 'retail', score: 1.6, topics: ['wellness'], on_topic_total: 8 },
|
|
269
|
+
{ title: 'Adaptogenic Sparkling Waters', brandNames: ['Recess', 'Kin'], graphId: 'retail', score: 1.7, topics: ['beverage'], on_topic_total: 8 },
|
|
270
|
+
{ title: 'Off-topic Gaming trend', brandNames: ['PlayStation'], graphId: 'retail', score: 0.2, topics: ['gaming'], on_topic_total: 8 },
|
|
271
|
+
{ title: 'Off-topic Luxury trend', brandNames: ['Hermès'], graphId: 'retail', score: 0.1, topics: ['luxury'], on_topic_total: 8 }
|
|
272
|
+
];
|
|
273
|
+
const nextMoves = await generateNextMoves(rows, 'Gen Z beverage hydration trends', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, { total: 12, onTopicTotal: 8 });
|
|
274
|
+
assert.ok(nextMoves, 'nextMoves must be defined');
|
|
275
|
+
assert.strictEqual(nextMoves.thread?.kind, 'more_in_graph', 'Thread kind must be more_in_graph');
|
|
276
|
+
assert.strictEqual(nextMoves.thread?.graph_id, 'retail');
|
|
277
|
+
assert.ok(typeof nextMoves.thread?.remaining_count === 'number' && nextMoves.thread.remaining_count > 0, 'remaining_count must be > 0');
|
|
278
|
+
// Brands must be extracted strictly from on-topic rows (Liquid IV, Gatorade, Waterdrop, Recess, Kin)
|
|
279
|
+
// and NEVER from off-topic rows (PlayStation, Hermès)
|
|
280
|
+
assert.ok(nextMoves.specific?.brands && nextMoves.specific.brands.length > 0, 'specific.brands must be populated');
|
|
281
|
+
const ON_TOPIC_BEVERAGE_BRANDS = new Set(['Liquid IV', 'Gatorade', 'Waterdrop', 'Recess', 'Kin']);
|
|
282
|
+
for (const b of nextMoves.specific.brands) {
|
|
283
|
+
assert.ok(ON_TOPIC_BEVERAGE_BRANDS.has(b), `Brand "${b}" must be from on-topic rows`);
|
|
284
|
+
assert.notStrictEqual(b, 'PlayStation', 'Must not contain off-topic PlayStation');
|
|
285
|
+
assert.notStrictEqual(b, 'Hermès', 'Must not contain off-topic Hermès');
|
|
286
|
+
}
|
|
287
|
+
console.log('✅ Test 12 Passed: Pinned regression test against 1.46.23 envelope for search_graph');
|
|
288
|
+
}
|
|
289
|
+
// Test 13: Lululemon competitor filter with explicit allowed athletic/apparel list
|
|
290
|
+
{
|
|
291
|
+
const ALLOWED_LULULEMON_COMPETITORS = new Set([
|
|
292
|
+
'Nike', 'Adidas', 'Alo', 'Alo Yoga', 'Vuori', 'Athleta',
|
|
293
|
+
'Under Armour', 'Gymshark', 'On', 'On Running', 'Puma',
|
|
294
|
+
'New Balance', 'Lorna Jane', 'Sweaty Betty', "Arc'teryx"
|
|
295
|
+
]);
|
|
296
|
+
// Simulated Lululemon footprint trends (sports graph)
|
|
297
|
+
const sportsFootprint = [
|
|
298
|
+
{ trend_name: 'Studio Athletic Performance', brandNames: ['Alo', 'Vuori'], graphId: 'sports', signal_score: 180 },
|
|
299
|
+
{ trend_name: 'Technical Running Apparel', brandNames: ['Nike', 'On Running'], graphId: 'sports', signal_score: 160 }
|
|
300
|
+
];
|
|
301
|
+
const nextMovesWithSports = await generateNextMoves(sportsFootprint, 'Lululemon', ['sports'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, {
|
|
302
|
+
isBrandTracker: true,
|
|
303
|
+
competitiveLandscape: ['Alo', 'Vuori'],
|
|
304
|
+
brandDisplayName: 'Lululemon',
|
|
305
|
+
earningsStatsSource: "Lululemon's latest earnings and financial results"
|
|
306
|
+
});
|
|
307
|
+
if (nextMovesWithSports.specific?.brands) {
|
|
308
|
+
for (const b of nextMovesWithSports.specific.brands) {
|
|
309
|
+
assert.ok(ALLOWED_LULULEMON_COMPETITORS.has(b), `Brand "${b}" must be in allowed athletic/apparel competitor list`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Simulated Lululemon with no sector-matching competitors (e.g. noise only -> competitiveLandscape empty)
|
|
313
|
+
const nextMovesEmptyCompetitors = await generateNextMoves(sportsFootprint, 'Lululemon', ['sports'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, {
|
|
314
|
+
isBrandTracker: true,
|
|
315
|
+
competitiveLandscape: [], // <1 competitor survives
|
|
316
|
+
brandDisplayName: 'Lululemon',
|
|
317
|
+
earningsStatsSource: "Lululemon's latest earnings and financial results"
|
|
318
|
+
});
|
|
319
|
+
assert.strictEqual(nextMovesEmptyCompetitors.specific?.brands, undefined, 'specific.brands must be undefined when competitiveLandscape is empty');
|
|
320
|
+
assert.strictEqual(nextMovesEmptyCompetitors.specific?.statistics_source, "Lululemon's latest earnings and financial results");
|
|
321
|
+
const closing = renderConsultClosingEnvelope(nextMovesEmptyCompetitors);
|
|
322
|
+
assert.ok(!closing.text.includes('La Mer'), 'Must never mention La Mer');
|
|
323
|
+
assert.ok(!closing.text.includes('NCR'), 'Must never mention NCR');
|
|
324
|
+
assert.ok(!closing.text.includes('Discover'), 'Must never mention Discover');
|
|
325
|
+
console.log('✅ Test 13 Passed: Lululemon competitor filter strictly adheres to athletic/apparel allowed set or omits clause');
|
|
326
|
+
}
|
|
327
|
+
// Test 14: Consult shelf score floor (query with low relevance to domain graphs cleanly omits sentence 2)
|
|
328
|
+
{
|
|
329
|
+
const mockResult = {
|
|
330
|
+
coverage: 'in',
|
|
331
|
+
report: 'Earned media and podcast strategies.',
|
|
332
|
+
sources_used: [{ title: 'Podcast Guesting Tips', type: 'own_graph', graph_id: 'james-colistra-earned-media-and-podcast' }],
|
|
333
|
+
expert_thread: {
|
|
334
|
+
on_topic_total: 4,
|
|
335
|
+
cited_count: 1,
|
|
336
|
+
uncited_themes: ['Host Alignment'],
|
|
337
|
+
next_angle: 'We can explore host alignment and podcast pitching strategies next.'
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'podcast guest tips', 'james-colistra-earned-media-and-podcast', undefined, mockGraphs, mockAnalysts);
|
|
341
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
342
|
+
assert.strictEqual(nextMoves.consult_envelope.shelf_line, undefined, 'shelf_line must be omitted when candidate graphs score below 0.10 floor');
|
|
343
|
+
assert.strictEqual(nextMoves.shelf, undefined, 'shelf must be undefined below floor');
|
|
344
|
+
const rendered = renderConsultClosingEnvelope(nextMoves);
|
|
345
|
+
assert.strictEqual(rendered.lines.length, 2, 'Must render exactly 2 sentences when shelf is below 0.10 floor');
|
|
346
|
+
assert.ok(!rendered.text.includes('Retail Strategy & Innovation'), 'Must not merchandise unrelated retail graph');
|
|
347
|
+
assert.ok(!rendered.text.includes('Beauty & Wellness'), 'Must not merchandise unrelated beauty graph');
|
|
348
|
+
console.log('✅ Test 14 Passed: Consult shelf score floor cleanly omits sentence 2 on low relevance');
|
|
349
|
+
}
|
|
350
|
+
// Test 15: Suggest-backed statistics_source (Neil's collectibles query)
|
|
351
|
+
{
|
|
352
|
+
clearSuggestCacheForTesting();
|
|
353
|
+
const mockSuggestFn = async (q) => {
|
|
354
|
+
return {
|
|
355
|
+
sources: [
|
|
356
|
+
{ id: 'google_trends', name: 'Google Trends' },
|
|
357
|
+
{ id: 'amazon_price', name: 'Amazon Price Intelligence' },
|
|
358
|
+
{ id: 'draft_source', name: 'Draft Source', returns_draft: true }
|
|
359
|
+
]
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
const rows = [{ title: 'Collectible card trading volumes', graphId: 'retail' }];
|
|
363
|
+
const nextMoves = await generateNextMoves(rows, 'collectible card trading and sports memorabilia market', ['retail'], 'thin', undefined, undefined, mockGraphs, mockAnalysts, { suggestFn: mockSuggestFn });
|
|
364
|
+
assert.ok(nextMoves.specific?.statistics_source, 'statistics_source must be populated');
|
|
365
|
+
assert.strictEqual(nextMoves.specific.statistics_source, 'Google Trends and Amazon Price Intelligence', 'statistics_source must format first 2 non-draft public source names');
|
|
366
|
+
console.log('✅ Test 15 Passed: Suggest-backed statistics_source formats public names and ignores draft sources');
|
|
367
|
+
}
|
|
368
|
+
// Test 16: Suggest timeout / error fallback to regex branch
|
|
369
|
+
{
|
|
370
|
+
clearSuggestCacheForTesting();
|
|
371
|
+
const mockTimeoutSuggestFn = async (q) => {
|
|
372
|
+
throw new Error('suggest timeout (3000ms)');
|
|
373
|
+
};
|
|
374
|
+
const rows = [{ title: 'Retail spending surge', graphId: 'retail' }];
|
|
375
|
+
const nextMoves = await generateNextMoves(rows, 'retail consumer spending market trends', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, { suggestFn: mockTimeoutSuggestFn });
|
|
376
|
+
assert.ok(nextMoves.specific?.statistics_source, 'statistics_source must fall back to regex branch');
|
|
377
|
+
assert.strictEqual(nextMoves.specific.statistics_source, 'Census retail trade and spending data');
|
|
378
|
+
console.log('✅ Test 16 Passed: Suggest timeout / failure falls back to existing regex branch');
|
|
379
|
+
}
|
|
380
|
+
// Test 17: Cost silence — pricing inquiries point to pricing URL with no currency figures
|
|
381
|
+
{
|
|
382
|
+
const pricingUrl = 'https://fodda.ai/pricing';
|
|
383
|
+
assert.strictEqual(pricingUrl, 'https://fodda.ai/pricing');
|
|
384
|
+
console.log('✅ Test 17 Passed: Pricing routes cleanly to https://fodda.ai/pricing');
|
|
385
|
+
}
|
|
386
|
+
// Test 18: Booking rate_display is preserved
|
|
387
|
+
{
|
|
388
|
+
const mockBookACall = {
|
|
389
|
+
url: 'https://cal.com/jeremy-smith/30min',
|
|
390
|
+
rate_display: '$500/hr'
|
|
391
|
+
};
|
|
392
|
+
assert.strictEqual(mockBookACall.rate_display, '$500/hr', 'rate_display must be preserved verbatim for book_a_call');
|
|
393
|
+
console.log('✅ Test 18 Passed: Human booking preserves rate_display verbatim');
|
|
394
|
+
}
|
|
395
|
+
// Test 19: Small graph (trend_count <= 15) suppresses more_in_graph
|
|
396
|
+
{
|
|
397
|
+
const smallReportGraph = {
|
|
398
|
+
graph_id: 'report-sustainable-packaging',
|
|
399
|
+
name: 'Sustainable Packaging 2026',
|
|
400
|
+
graph_type: 'industry report',
|
|
401
|
+
trend_count: 10,
|
|
402
|
+
topics: ['packaging', 'sustainability']
|
|
403
|
+
};
|
|
404
|
+
const extendedGraphs = [...mockGraphs, smallReportGraph];
|
|
405
|
+
const rows = [
|
|
406
|
+
{ title: 'Mushroom packaging', graphId: 'report-sustainable-packaging', on_topic_total: 8, total: 8 },
|
|
407
|
+
{ title: 'Seaweed alternatives', graphId: 'report-sustainable-packaging', on_topic_total: 8, total: 8 }
|
|
408
|
+
];
|
|
409
|
+
const nextMoves = await generateNextMoves(rows, 'biodegradable mushroom packaging materials', ['report-sustainable-packaging'], 'ok', undefined, undefined, extendedGraphs, mockAnalysts);
|
|
410
|
+
assert.notStrictEqual(nextMoves.thread?.kind, 'more_in_graph', 'Small graph must not emit more_in_graph');
|
|
411
|
+
if (nextMoves.thread?.adjacent) {
|
|
412
|
+
assert.notStrictEqual(nextMoves.thread.adjacent.graph_id, 'report-sustainable-packaging', 'adjacent.graph_id must not equal the searched graph');
|
|
413
|
+
}
|
|
414
|
+
console.log('✅ Test 19 Passed: Small graph (trend_count <= 15) suppresses more_in_graph');
|
|
415
|
+
}
|
|
416
|
+
// Test 20: Exhausted graph (on_topic_total >= 0.6 * trend_count) suppresses more_in_graph
|
|
417
|
+
{
|
|
418
|
+
const mediumGraph = {
|
|
419
|
+
graph_id: 'dentsu-future-of-commerce',
|
|
420
|
+
name: 'Dentsu Future of Commerce',
|
|
421
|
+
graph_type: 'industry report',
|
|
422
|
+
curator: 'Dentsu',
|
|
423
|
+
trend_count: 20,
|
|
424
|
+
topics: ['retail', 'commerce']
|
|
425
|
+
};
|
|
426
|
+
const extendedGraphs = [...mockGraphs, mediumGraph];
|
|
427
|
+
const rows = [
|
|
428
|
+
{ title: 'Autonomous checkout', graphId: 'dentsu-future-of-commerce', on_topic_total: 14, total: 14 },
|
|
429
|
+
{ title: 'Social commerce live streams', graphId: 'dentsu-future-of-commerce', on_topic_total: 14, total: 14 }
|
|
430
|
+
];
|
|
431
|
+
const nextMoves = await generateNextMoves(rows, 'future of autonomous commerce and checkout', ['dentsu-future-of-commerce'], 'ok', undefined, undefined, extendedGraphs, mockAnalysts);
|
|
432
|
+
assert.notStrictEqual(nextMoves.thread?.kind, 'more_in_graph', 'Exhausted graph must not emit more_in_graph');
|
|
433
|
+
console.log('✅ Test 20 Passed: Exhausted graph (>=60% returned) suppresses more_in_graph');
|
|
434
|
+
}
|
|
435
|
+
// Test 21: Brand publisher exclusion (dentsu, havas, psfk, niq)
|
|
436
|
+
{
|
|
437
|
+
const rows = [
|
|
438
|
+
{
|
|
439
|
+
title: 'Digital retail media innovation',
|
|
440
|
+
graphId: 'retail',
|
|
441
|
+
brandNames: ['Dentsu', 'Nike', 'Havas'],
|
|
442
|
+
source_label: 'Dentsu Living Commerce (Dentsu)'
|
|
443
|
+
}
|
|
444
|
+
];
|
|
445
|
+
const nextMoves = await generateNextMoves(rows, 'digital retail media innovation', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts);
|
|
446
|
+
assert.ok(nextMoves.specific?.brands, 'specific.brands should be present');
|
|
447
|
+
assert.deepStrictEqual(nextMoves.specific?.brands, ['Nike'], 'Must exclude publisher/curator tokens like Dentsu and Havas');
|
|
448
|
+
console.log('✅ Test 21 Passed: Brand extraction excludes publisher and curator tokens');
|
|
449
|
+
}
|
|
450
|
+
// Test 22: Expert reason word-boundary formatting
|
|
451
|
+
{
|
|
452
|
+
const analystLongExpertise = {
|
|
453
|
+
analyst_id: 'supply-chain-guru',
|
|
454
|
+
name: 'Supply Chain Guru',
|
|
455
|
+
description: 'Covers complex international omnichannel logistics and freight forwarding operations across North America and Europe',
|
|
456
|
+
expert_in: 'omnichannel retail supply chain logistics strategy and forecasting',
|
|
457
|
+
status: 'active'
|
|
458
|
+
};
|
|
459
|
+
const extendedAnalysts = [...mockAnalysts, analystLongExpertise];
|
|
460
|
+
const rows = [{ title: 'Omnichannel logistics', graphId: 'retail' }];
|
|
461
|
+
const nextMoves = await generateNextMoves(rows, 'omnichannel retail supply chain logistics strategy and forecasting', ['retail'], 'ok', undefined, undefined, mockGraphs, extendedAnalysts);
|
|
462
|
+
assert.ok(nextMoves.specific?.expert, 'specific.expert must be present');
|
|
463
|
+
const reason = nextMoves.specific.expert.reason;
|
|
464
|
+
assert.ok(reason.startsWith('covers '), 'reason must start with covers');
|
|
465
|
+
assert.ok(reason.endsWith(' directly'), 'reason must end with directly');
|
|
466
|
+
// Ensure length constraint and no mid-word cutoff
|
|
467
|
+
const lane = reason.replace(/^covers /, '').replace(/ directly$/, '');
|
|
468
|
+
assert.ok(lane.length <= 60, 'lane must be <= 60 characters');
|
|
469
|
+
assert.ok(!lane.endsWith(' fo'), 'Must not truncate mid-word');
|
|
470
|
+
console.log(`✅ Test 22 Passed: Expert reason formatted cleanly on word boundary: "${reason}"`);
|
|
471
|
+
}
|
|
472
|
+
// Test 23: adjacent.graph_id is never equal to thread.graph_id or any searched graph
|
|
473
|
+
{
|
|
474
|
+
const nextMoves = await generateNextMoves([], 'quantum computing in banking', ['retail'], 'empty', undefined, undefined, mockGraphs, mockAnalysts);
|
|
475
|
+
if (nextMoves.thread?.adjacent) {
|
|
476
|
+
assert.notStrictEqual(nextMoves.thread.adjacent.graph_id, 'retail', 'adjacent.graph_id must not be searched graph retail');
|
|
477
|
+
assert.ok(nextMoves.thread.adjacent.graph_id.length > 0, 'adjacent.graph_id must be non-empty');
|
|
478
|
+
}
|
|
479
|
+
console.log('✅ Test 23 Passed: adjacent.graph_id is never equal to searched graph or self');
|
|
480
|
+
}
|
|
481
|
+
// Test 24: Parallel suggest promise passed into options resolves properly
|
|
482
|
+
{
|
|
483
|
+
clearSuggestCacheForTesting();
|
|
484
|
+
const parallelPromise = Promise.resolve({
|
|
485
|
+
sources: [
|
|
486
|
+
{ id: 'google_trends', name: 'Google Trends' },
|
|
487
|
+
{ id: 'fred', name: 'FRED Economic Data' }
|
|
488
|
+
]
|
|
489
|
+
});
|
|
490
|
+
const rows = [{ title: 'Trading card secondary market values', graphId: 'retail' }];
|
|
491
|
+
const nextMoves = await generateNextMoves(rows, 'what are the trends in the collectible space, particularly trading cards', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts, { suggestPromise: parallelPromise });
|
|
492
|
+
assert.ok(nextMoves.specific?.statistics_source, 'statistics_source must be populated from suggestPromise');
|
|
493
|
+
assert.strictEqual(nextMoves.specific.statistics_source, 'Google Trends and FRED Economic Data', 'statistics_source must use parallel suggest promise result');
|
|
494
|
+
console.log('✅ Test 24 Passed: Parallel suggest promise resolved cleanly without racing');
|
|
495
|
+
}
|
|
496
|
+
// Test 25: Mega-trend brand guard excludes rows with brand_count > 30 or brandNames.length >= 10
|
|
497
|
+
{
|
|
498
|
+
const megaTrendRows = [
|
|
499
|
+
{
|
|
500
|
+
title: 'Experiential Loyalty',
|
|
501
|
+
graphId: 'retail',
|
|
502
|
+
brand_count: 734,
|
|
503
|
+
brandNames: ['PlayStation', 'Hermès', 'Nike', 'Apple', 'Target', 'Starbucks', 'Sephora', 'Gucci', 'LVMH', 'Prada']
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
title: 'Uncounted Mega Trend',
|
|
507
|
+
graphId: 'retail',
|
|
508
|
+
brandNames: ['Brand1', 'Brand2', 'Brand3', 'Brand4', 'Brand5', 'Brand6', 'Brand7', 'Brand8', 'Brand9', 'Brand10']
|
|
509
|
+
}
|
|
510
|
+
];
|
|
511
|
+
const nextMoves = await generateNextMoves(megaTrendRows, 'what are the trends in the collectible space, particularly trading cards', ['retail'], 'ok', undefined, undefined, mockGraphs, mockAnalysts);
|
|
512
|
+
assert.strictEqual(nextMoves.specific?.brands, undefined, 'specific.brands must be undefined when only mega-trend rows with >30 brands or >=10 brandNames are present');
|
|
513
|
+
console.log('✅ Test 25 Passed: Mega-trend brand guard cleanly excludes roster-like brand rows');
|
|
514
|
+
}
|
|
515
|
+
// Test 26: Thin-coverage expert routing picks lane match (Nathan Grotticelli) over first roster entry (Anu Lingala)
|
|
516
|
+
{
|
|
517
|
+
const roster = [
|
|
518
|
+
{
|
|
519
|
+
analyst_id: 'anu-lingala',
|
|
520
|
+
name: 'Anu Lingala',
|
|
521
|
+
status: 'active',
|
|
522
|
+
topics: ['cultural macro-trends', 'lifestyle', 'culture'],
|
|
523
|
+
description: 'Cultural macro-trends and consumer lifestyle shifts',
|
|
524
|
+
expert_in: 'cultural macro-trends and lifestyle forecasting',
|
|
525
|
+
outside_their_lane: 'performance marketing, creative effectiveness benchmarks, ad tech',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
analyst_id: 'nathan-grotticelli',
|
|
529
|
+
name: 'Nathan Grotticelli',
|
|
530
|
+
status: 'active',
|
|
531
|
+
topics: ['performance marketing', 'creative effectiveness', 'growth marketing'],
|
|
532
|
+
description: 'Performance marketing and creative effectiveness benchmarks',
|
|
533
|
+
expert_in: 'performance marketing, ad creative effectiveness benchmarks, and growth analytics',
|
|
534
|
+
outside_their_lane: 'haute couture fashion, fine jewelry',
|
|
535
|
+
}
|
|
536
|
+
];
|
|
537
|
+
const thinRows = [{ title: 'Ad retention rate metric', graphId: 'retail' }];
|
|
538
|
+
const nextMoves = await generateNextMoves(thinRows, 'creative effectiveness benchmarks', ['retail'], 'thin', undefined, undefined, mockGraphs, roster);
|
|
539
|
+
assert.ok(nextMoves.specific?.expert, 'specific.expert must be present');
|
|
540
|
+
assert.strictEqual(nextMoves.specific.expert.analyst_id, 'nathan-grotticelli', 'Must pick Nathan Grotticelli for creative effectiveness benchmarks, NOT Anu Lingala');
|
|
541
|
+
assert.strictEqual(nextMoves.specific.expert.display_name, 'Nathan Grotticelli');
|
|
542
|
+
assert.ok(nextMoves.specific.expert.reason.startsWith('closest expert lane'), `Reason must be honest and status-aware on thin coverage, got: ${nextMoves.specific.expert.reason}`);
|
|
543
|
+
console.log(`✅ Test 26 Passed: Thin-coverage scored expert routing picked Nathan over Anu: "${nextMoves.specific.expert.reason}"`);
|
|
544
|
+
}
|
|
545
|
+
// Test 27: Blind spot exclusion prevents out-of-lane recommendations
|
|
546
|
+
{
|
|
547
|
+
const roster = [
|
|
548
|
+
{
|
|
549
|
+
analyst_id: 'anu-lingala',
|
|
550
|
+
name: 'Anu Lingala',
|
|
551
|
+
status: 'active',
|
|
552
|
+
topics: ['cultural macro-trends', 'lifestyle', 'culture'],
|
|
553
|
+
description: 'Cultural macro-trends and consumer lifestyle shifts',
|
|
554
|
+
expert_in: 'cultural macro-trends and lifestyle forecasting',
|
|
555
|
+
outside_their_lane: 'performance marketing, ad creative benchmarks, b2b saas',
|
|
556
|
+
}
|
|
557
|
+
];
|
|
558
|
+
const nextMoves = await generateNextMoves([], 'performance marketing and ad creative benchmarks', [], 'empty', undefined, undefined, mockGraphs, roster);
|
|
559
|
+
assert.strictEqual(nextMoves.specific?.expert, undefined, 'Analyst with matching blind spot / outside_their_lane must be disqualified');
|
|
560
|
+
console.log('✅ Test 27 Passed: Blind spot exclusion prevented out-of-lane expert recommendation');
|
|
561
|
+
}
|
|
562
|
+
// Test 28: Zero lane-fit query omits specific.expert entirely
|
|
563
|
+
{
|
|
564
|
+
const nextMoves = await generateNextMoves([], 'superconducting quantum qubit coherence', [], 'empty', undefined, undefined, mockGraphs, mockAnalysts);
|
|
565
|
+
assert.strictEqual(nextMoves.specific?.expert, undefined, 'No-lane-fit query must omit specific.expert rather than naming first roster entry');
|
|
566
|
+
console.log('✅ Test 28 Passed: Zero lane-fit query cleanly omitted specific.expert');
|
|
567
|
+
}
|
|
568
|
+
// Test 29: Experiential & Pet Food query expansions route to Retail Strategy shelf
|
|
569
|
+
{
|
|
570
|
+
setCachedCatalogForTesting({ version: '1.0', generated_at: new Date().toISOString(), graph_count: mockGraphs.length, graphs: mockGraphs }, mockAnalysts);
|
|
571
|
+
const mockResult = {
|
|
572
|
+
coverage: 'adjacent',
|
|
573
|
+
report: 'Spectacle and myth-making for pet food activations.',
|
|
574
|
+
sources_used: [],
|
|
575
|
+
expert_thread: {
|
|
576
|
+
on_topic_total: 0,
|
|
577
|
+
cited_count: 0,
|
|
578
|
+
uncited_themes: [],
|
|
579
|
+
brands: [],
|
|
580
|
+
next_angle: null
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'i want to put on an experiential activation for a Pet Food company', 'josephine-baker', {}, mockGraphs, mockAnalysts);
|
|
584
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
585
|
+
assert.ok(nextMoves.consult_envelope.shelf_line, 'Shelf line must be populated for experiential activation query');
|
|
586
|
+
assert.ok(nextMoves.consult_envelope.shelf_line.includes('Retail Strategy'), `Shelf line must surface Retail Strategy graph for experiential activation, got: "${nextMoves.consult_envelope.shelf_line}"`);
|
|
587
|
+
console.log('✅ Test 29 Passed: Experiential activation query expanded and surfaced Retail Strategy on shelf');
|
|
588
|
+
}
|
|
589
|
+
// Test 30: Zero shelf candidate graphs with deliverable offering falls back to deliverable line
|
|
590
|
+
{
|
|
591
|
+
const analystsWithOffering = [
|
|
592
|
+
{
|
|
593
|
+
analyst_id: 'marketing-lead',
|
|
594
|
+
name: 'Marketing Lead',
|
|
595
|
+
status: 'Active',
|
|
596
|
+
topics: ['marketing'],
|
|
597
|
+
description: 'Marketing strategy',
|
|
598
|
+
offerings: [{ offering_key: 'marketing_plan', display_name: 'Marketing Plan' }]
|
|
599
|
+
}
|
|
600
|
+
];
|
|
601
|
+
setCachedCatalogForTesting({ version: '1.0', generated_at: new Date().toISOString(), graph_count: 0, graphs: [] }, analystsWithOffering);
|
|
602
|
+
const mockResult = {
|
|
603
|
+
coverage: 'in',
|
|
604
|
+
report: 'Niche topic analysis.',
|
|
605
|
+
sources_used: [],
|
|
606
|
+
expert_thread: {
|
|
607
|
+
on_topic_total: 0,
|
|
608
|
+
cited_count: 0,
|
|
609
|
+
uncited_themes: [],
|
|
610
|
+
brands: [],
|
|
611
|
+
next_angle: null
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
const nextMoves = generateConsultNextMoves(mockResult, 'superconducting quantum qubit coherence', 'marketing-lead', {}, [], // No graphs
|
|
615
|
+
analystsWithOffering);
|
|
616
|
+
assert.ok(nextMoves.consult_envelope, 'consult_envelope must be populated');
|
|
617
|
+
assert.strictEqual(nextMoves.consult_envelope.shelf_line, 'Marketing Lead takes scoped briefs on this if you need a deliverable.', 'Must output deliverable line when shelf candidate graphs are empty');
|
|
618
|
+
console.log('✅ Test 30 Passed: Zero shelf candidate graphs cleanly fell back to deliverable offering line');
|
|
619
|
+
}
|
|
620
|
+
// Test 31: Pacific Islands regression guard (catalog[0] is never blindly grabbed on thin/empty query)
|
|
621
|
+
{
|
|
622
|
+
const catalogWithPacific = [
|
|
623
|
+
{
|
|
624
|
+
graph_id: 'pdh_pacific',
|
|
625
|
+
name: 'Pacific Islands Data Hub',
|
|
626
|
+
graph_type: 'domain',
|
|
627
|
+
status: 'live',
|
|
628
|
+
topics: ['pacific', 'fiji', 'oceania'],
|
|
629
|
+
trend_count: 50,
|
|
630
|
+
evidence_count: 200,
|
|
631
|
+
},
|
|
632
|
+
...mockGraphs,
|
|
633
|
+
];
|
|
634
|
+
const nextMoves = await generateNextMoves([], 'enterprise cloud computing security', ['retail'], 'empty', undefined, undefined, catalogWithPacific, mockAnalysts);
|
|
635
|
+
assert.ok(nextMoves, 'nextMoves must be defined');
|
|
636
|
+
assert.notStrictEqual(nextMoves.thread?.adjacent?.graph_id, 'pdh_pacific', 'Must NEVER fall back to pdh_pacific on unrelated query');
|
|
637
|
+
assert.strictEqual(nextMoves.thread?.adjacent, undefined, 'No adjacent room should be hallucinated without relevance');
|
|
638
|
+
assert.strictEqual(nextMoves.thread?.kind, 'honest_thin');
|
|
639
|
+
console.log('✅ Test 31 Passed: Pacific Islands blind fallback suppressed on unrelated query');
|
|
640
|
+
}
|
|
641
|
+
// Test 32: Beauty & Skincare query cleanly omits Piers Fawkes (no false expert recommendation)
|
|
642
|
+
{
|
|
643
|
+
const rosterWithPiers = [
|
|
644
|
+
{
|
|
645
|
+
analyst_id: 'piers-fawkes-psfk',
|
|
646
|
+
name: 'Piers Fawkes',
|
|
647
|
+
status: 'active',
|
|
648
|
+
category: 'human_agent',
|
|
649
|
+
is_human_agent: true,
|
|
650
|
+
topics: ['retail innovation', 'consumer trends', 'customer experience'],
|
|
651
|
+
description: 'Founder & CEO of PSFK, retail innovation and consumer trend expert',
|
|
652
|
+
expert_in: 'retail innovation, store design, future of retail',
|
|
653
|
+
outside_their_lane: 'deep learning architecture, financial engineering',
|
|
654
|
+
}
|
|
655
|
+
];
|
|
656
|
+
const nextMoves = await generateNextMoves([], 'beauty and skincare trends', ['beauty'], 'empty', undefined, undefined, mockGraphs, rosterWithPiers);
|
|
657
|
+
assert.strictEqual(nextMoves.specific?.expert, undefined, 'Piers Fawkes must NOT be recommended on beauty and skincare query');
|
|
658
|
+
console.log('✅ Test 32 Passed: Piers Fawkes suppressed on beauty and skincare query (no explicit topic match)');
|
|
659
|
+
}
|
|
660
|
+
// Test 33: 4-tier agent intent prioritization
|
|
661
|
+
{
|
|
662
|
+
const tieredRoster = [
|
|
663
|
+
{
|
|
664
|
+
analyst_id: 'brand-cmo',
|
|
665
|
+
name: 'Brand CMO',
|
|
666
|
+
status: 'active',
|
|
667
|
+
category: 'c_suite_agent',
|
|
668
|
+
is_c_suite_agent: true,
|
|
669
|
+
topics: ['marketing strategy', 'brand governance', 'enterprise marketing'],
|
|
670
|
+
description: 'Corporate CMO lens grounded in SEC filings and earnings transcripts',
|
|
671
|
+
expert_in: 'brand strategy, corporate earnings, executive marketing decisions',
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
analyst_id: 'john-ruskin',
|
|
675
|
+
name: 'John Ruskin',
|
|
676
|
+
status: 'active',
|
|
677
|
+
category: 'classic_agent',
|
|
678
|
+
is_classic_agent: true,
|
|
679
|
+
topics: ['art criticism', 'architectural theory', 'craftsmanship', 'aesthetic philosophy'],
|
|
680
|
+
description: 'Victorian art critic and social thinker',
|
|
681
|
+
expert_in: 'craftsmanship, aesthetic philosophy, moral critique of industrialism',
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
analyst_id: 'ben-dietz-sic',
|
|
685
|
+
name: 'Ben Dietz',
|
|
686
|
+
status: 'active',
|
|
687
|
+
category: 'human_agent',
|
|
688
|
+
is_human_agent: true,
|
|
689
|
+
topics: ['streetwear', 'youth culture', 'music', 'subcultures'],
|
|
690
|
+
description: 'Cultural strategist and host of [SIC] Weekly',
|
|
691
|
+
expert_in: 'streetwear culture, underground movements, youth trends',
|
|
692
|
+
}
|
|
693
|
+
];
|
|
694
|
+
// Case A: Corporate / earnings query picks C-Suite Agent
|
|
695
|
+
const corporateMoves = await generateNextMoves([], 'quarterly brand marketing earnings review', ['retail'], 'thin', undefined, undefined, mockGraphs, tieredRoster);
|
|
696
|
+
assert.strictEqual(corporateMoves.specific?.expert?.analyst_id, 'brand-cmo', 'Must pick Brand CMO for corporate marketing earnings');
|
|
697
|
+
assert.strictEqual(corporateMoves.specific?.expert?.category, 'c_suite_agent');
|
|
698
|
+
// Case B: Aesthetics & art criticism picks Classic Agent
|
|
699
|
+
const aestheticMoves = await generateNextMoves([], 'moral philosophy of art and craftsmanship in design', ['retail'], 'thin', undefined, undefined, mockGraphs, tieredRoster);
|
|
700
|
+
assert.strictEqual(aestheticMoves.specific?.expert?.analyst_id, 'john-ruskin', 'Must pick John Ruskin for craftsmanship and aesthetic philosophy');
|
|
701
|
+
assert.strictEqual(aestheticMoves.specific?.expert?.category, 'classic_agent');
|
|
702
|
+
// Case C: Contemporary culture & streetwear picks living Human Agent
|
|
703
|
+
const cultureMoves = await generateNextMoves([], 'streetwear youth subcultures and underground music', ['retail'], 'thin', undefined, undefined, mockGraphs, tieredRoster);
|
|
704
|
+
assert.strictEqual(cultureMoves.specific?.expert?.analyst_id, 'ben-dietz-sic', 'Must pick Ben Dietz for streetwear and youth culture');
|
|
705
|
+
assert.strictEqual(cultureMoves.specific?.expert?.category, 'human_agent');
|
|
706
|
+
console.log('✅ Test 33 Passed: 4-tier agent intent prioritization correctly routed C-Suite, Classic, and Human agents');
|
|
707
|
+
}
|
|
708
|
+
console.log('\nAll Next Moves unit tests passed successfully!');
|
|
709
|
+
}
|
|
710
|
+
runTests().catch(err => {
|
|
711
|
+
console.error('Test run failed:', err);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
});
|
|
714
|
+
//# sourceMappingURL=test_next_moves.js.map
|