feat(tests): add comprehensive test suite with 1493 tests across 4 phases

Phase 1 - Foundation:
- Add pytest, pyproject.toml, requirements-dev.txt
- 593 smoke tests (324 syntax + 269 argparse --help) for all Python scripts
- Harden CI: remove || true from compileall, expand to all 9 directories, add pytest step

Phase 2 - Calculator unit tests:
- RICE prioritizer: formula, prioritization, portfolio analysis, roadmap
- DCF valuation: WACC, cash flow projections, terminal value, sensitivity
- Financial ratios: profitability, liquidity, leverage, efficiency, valuation
- Campaign ROI: metrics, benchmarks, portfolio summary
- Funnel analyzer: stage metrics, bottleneck detection, segment comparison
- OKR tracker: numeric/percentage/milestone/boolean KR scoring, status

Phase 3 - Parser and compliance tests:
- SEO checker: HTML parsing, scoring, heading hierarchy, alt text, word count
- Commit linter: conventional commit regex, lint report, file input
- GDPR compliance: pattern detection, file scanning, project analysis

Phase 4 - Integration tests:
- 671 skill integrity tests: frontmatter, H1 headings, scripts dirs, references

Bug fixes found by tests:
- Fix duplicate --reason argparse arg in document_version_control.py

https://claude.ai/code/session_01MsVmZoAsPvLv7rAGDBGTbL
This commit is contained in:
Claude
2026-03-30 19:54:00 +00:00
parent 235c063701
commit 36678cd00d
18 changed files with 1849 additions and 5 deletions

View File

@@ -50,6 +50,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install yamllint==1.35.1 check-jsonschema==0.28.4 safety==3.2.4
pip install -r requirements-dev.txt
- name: Set up Node.js
uses: actions/setup-node@v4
@@ -71,9 +72,13 @@ jobs:
! -name "smart-sync.yml" \
-exec check-jsonschema --builtin-schema github-workflows {} + || true
- name: Python syntax check
- name: Python syntax check (blocking)
run: |
python -m compileall marketing-skill product-team c-level-advisor engineering-team ra-qm-team || true
python -m compileall marketing-skill product-team c-level-advisor engineering-team ra-qm-team engineering business-growth finance project-management scripts
- name: Run test suite
run: |
python -m pytest tests/ --tb=short -q
- name: Safety dependency audit (requirements*.txt)
run: |

5
pyproject.toml Normal file
View File

@@ -0,0 +1,5 @@
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"

View File

@@ -383,7 +383,7 @@ def main():
parser.add_argument("--approve", type=str, help="Approve document (doc_id)")
parser.add_argument("--approver", type=str, help="Approver name")
parser.add_argument("--withdraw", type=str, help="Withdraw document (doc_id)")
parser.add_argument("--reason", type=str, help="Withdrawal reason")
parser.add_argument("--withdraw-reason", type=str, help="Withdrawal reason")
parser.add_argument("--status", action="store_true", help="Show document status")
parser.add_argument("--matrix", action="store_true", help="Generate document matrix")
parser.add_argument("--output", choices=["text", "json"], default="text")
@@ -434,8 +434,8 @@ def main():
elif args.approve and args.approver:
success = dvc.approve_document(args.approve, args.approver, "QMS Manager")
print(f"{'✅ Approved' if success else '❌ Failed'} document {args.approve}")
elif args.withdraw and args.reason:
success = dvc.withdraw_document(args.withdraw, args.reason, "QMS Manager")
elif args.withdraw and args.withdraw_reason:
success = dvc.withdraw_document(args.withdraw, args.withdraw_reason, "QMS Manager")
print(f"{'✅ Withdrawn' if success else '❌ Failed'} document {args.withdraw}")
elif args.matrix:
matrix = dvc.generate_document_matrix()

1
requirements-dev.txt Normal file
View File

@@ -0,0 +1 @@
pytest>=8.0,<9.0

0
tests/__init__.py Normal file
View File

15
tests/conftest.py Normal file
View File

@@ -0,0 +1,15 @@
"""Shared fixtures and configuration for the test suite."""
import os
import sys
# Repository root
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def add_script_dir_to_path(script_path: str):
"""Add a script's parent directory to sys.path for imports."""
script_dir = os.path.dirname(os.path.abspath(script_path))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
return script_dir

163
tests/test_campaign_roi.py Normal file
View File

@@ -0,0 +1,163 @@
"""Unit tests for the Campaign ROI Calculator."""
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "marketing-skill", "campaign-analytics", "scripts"
))
from campaign_roi_calculator import (
safe_divide,
get_benchmark,
assess_performance,
calculate_campaign_metrics,
calculate_portfolio_summary,
)
class TestSafeDivide:
def test_normal(self):
assert safe_divide(10, 2) == 5.0
def test_zero_denominator(self):
assert safe_divide(10, 0) == 0.0
def test_custom_default(self):
assert safe_divide(10, 0, -1.0) == -1.0
class TestGetBenchmark:
def test_known_channel(self):
result = get_benchmark("ctr", "email")
assert result == (1.0, 2.5, 5.0)
def test_falls_back_to_default(self):
result = get_benchmark("ctr", "nonexistent_channel")
assert result == (0.5, 2.0, 5.0)
def test_unknown_metric(self):
result = get_benchmark("nonexistent_metric", "email")
assert result == (0, 0, 0)
class TestAssessPerformance:
def test_excellent_high_is_better(self):
assert assess_performance(10.0, (1.0, 3.0, 5.0), higher_is_better=True) == "excellent"
def test_good_high_is_better(self):
assert assess_performance(3.5, (1.0, 3.0, 5.0), higher_is_better=True) == "good"
def test_below_target_high_is_better(self):
assert assess_performance(1.5, (1.0, 3.0, 5.0), higher_is_better=True) == "below_target"
def test_underperforming_high_is_better(self):
assert assess_performance(0.5, (1.0, 3.0, 5.0), higher_is_better=True) == "underperforming"
def test_excellent_low_is_better(self):
# For cost metrics, lower is better
assert assess_performance(0.5, (1.0, 3.0, 5.0), higher_is_better=False) == "excellent"
def test_underperforming_low_is_better(self):
assert assess_performance(10.0, (1.0, 3.0, 5.0), higher_is_better=False) == "underperforming"
class TestCalculateCampaignMetrics:
@pytest.fixture
def campaign(self):
return {
"name": "Test Campaign",
"channel": "paid_search",
"spend": 1000.0,
"revenue": 5000.0,
"impressions": 100000,
"clicks": 3000,
"leads": 100,
"customers": 10,
}
def test_roi(self, campaign):
result = calculate_campaign_metrics(campaign)
# ROI = (5000 - 1000) / 1000 * 100 = 400%
assert result["metrics"]["roi_pct"] == 400.0
def test_roas(self, campaign):
result = calculate_campaign_metrics(campaign)
# ROAS = 5000 / 1000 = 5.0
assert result["metrics"]["roas"] == 5.0
def test_cpa(self, campaign):
result = calculate_campaign_metrics(campaign)
# CPA = 1000 / 10 = 100.0
assert result["metrics"]["cpa"] == 100.0
def test_ctr(self, campaign):
result = calculate_campaign_metrics(campaign)
# CTR = 3000 / 100000 * 100 = 3.0%
assert result["metrics"]["ctr_pct"] == 3.0
def test_cvr(self, campaign):
result = calculate_campaign_metrics(campaign)
# CVR = 10 / 100 * 100 = 10.0%
assert result["metrics"]["cvr_pct"] == 10.0
def test_profit(self, campaign):
result = calculate_campaign_metrics(campaign)
assert result["metrics"]["profit"] == 4000.0
def test_zero_customers(self):
campaign = {"name": "No Customers", "channel": "display", "spend": 500, "revenue": 0,
"impressions": 10000, "clicks": 50, "leads": 5, "customers": 0}
result = calculate_campaign_metrics(campaign)
assert result["metrics"]["cpa"] is None
assert result["metrics"]["cac"] is None
def test_zero_impressions(self):
campaign = {"name": "No Impressions", "channel": "email", "spend": 100, "revenue": 500,
"impressions": 0, "clicks": 0, "leads": 0, "customers": 0}
result = calculate_campaign_metrics(campaign)
assert result["metrics"]["ctr_pct"] is None
assert result["metrics"]["cpm"] is None
def test_unprofitable_campaign_flagged(self):
campaign = {"name": "Loser", "channel": "display", "spend": 1000, "revenue": 200,
"impressions": 50000, "clicks": 100, "leads": 5, "customers": 1}
result = calculate_campaign_metrics(campaign)
assert any("unprofitable" in f.lower() for f in result["flags"])
def test_benchmark_assessments_present(self, campaign):
result = calculate_campaign_metrics(campaign)
assert "ctr" in result["assessments"]
assert "benchmark_range" in result["assessments"]["ctr"]
class TestCalculatePortfolioSummary:
def test_aggregates_totals(self):
campaigns = [
calculate_campaign_metrics({
"name": "A", "channel": "email", "spend": 500, "revenue": 2000,
"impressions": 50000, "clicks": 1000, "leads": 50, "customers": 5,
}),
calculate_campaign_metrics({
"name": "B", "channel": "paid_search", "spend": 1000, "revenue": 4000,
"impressions": 100000, "clicks": 3000, "leads": 100, "customers": 10,
}),
]
summary = calculate_portfolio_summary(campaigns)
assert summary["total_spend"] == 1500
assert summary["total_revenue"] == 6000
assert summary["total_profit"] == 4500
assert summary["total_customers"] == 15
assert summary["total_campaigns"] == 2
def test_channel_summary(self):
campaigns = [
calculate_campaign_metrics({
"name": "A", "channel": "email", "spend": 500, "revenue": 2000,
"impressions": 50000, "clicks": 1000, "leads": 50, "customers": 5,
}),
]
summary = calculate_portfolio_summary(campaigns)
assert "email" in summary["channel_summary"]
assert summary["channel_summary"]["email"]["spend"] == 500

118
tests/test_commit_linter.py Normal file
View File

@@ -0,0 +1,118 @@
"""Unit tests for the Commit Linter (Conventional Commits)."""
import sys
import os
import tempfile
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "engineering", "changelog-generator", "scripts"
))
from commit_linter import lint, CONVENTIONAL_RE, lines_from_file, CLIError
class TestConventionalCommitRegex:
"""Test the regex pattern against various commit message formats."""
@pytest.mark.parametrize("msg", [
"feat: add user authentication",
"fix: resolve null pointer in parser",
"docs: update API documentation",
"refactor: simplify login flow",
"test: add integration tests for auth",
"build: upgrade webpack to v5",
"ci: add GitHub Actions workflow",
"chore: update dependencies",
"perf: optimize database queries",
"security: patch XSS vulnerability",
"deprecated: mark v1 API as deprecated",
"remove: drop legacy payment module",
])
def test_valid_types(self, msg):
assert CONVENTIONAL_RE.match(msg) is not None
@pytest.mark.parametrize("msg", [
"feat(auth): add OAuth2 support",
"fix(parser/html): handle malformed tags",
"docs(api.v2): update endpoint docs",
])
def test_valid_scopes(self, msg):
assert CONVENTIONAL_RE.match(msg) is not None
def test_breaking_change_marker(self):
assert CONVENTIONAL_RE.match("feat!: redesign API") is not None
assert CONVENTIONAL_RE.match("feat(api)!: breaking change") is not None
@pytest.mark.parametrize("msg", [
"Update readme",
"Fixed the bug",
"WIP: something",
"FEAT: uppercase type",
"feat:missing space",
"feat : extra space before colon",
"",
"merge: not a valid type",
])
def test_invalid_messages(self, msg):
assert CONVENTIONAL_RE.match(msg) is None
class TestLint:
def test_all_valid(self):
lines = [
"feat: add login",
"fix: resolve crash",
"docs: update README",
]
report = lint(lines)
assert report.total == 3
assert report.valid == 3
assert report.invalid == 0
assert report.violations == []
def test_mixed_valid_invalid(self):
lines = [
"feat: add login",
"Updated the readme",
"fix: resolve crash",
]
report = lint(lines)
assert report.total == 3
assert report.valid == 2
assert report.invalid == 1
assert "line 2" in report.violations[0]
def test_all_invalid(self):
lines = ["bad commit", "another bad one"]
report = lint(lines)
assert report.valid == 0
assert report.invalid == 2
def test_empty_input(self):
report = lint([])
assert report.total == 0
assert report.valid == 0
assert report.invalid == 0
class TestLinesFromFile:
def test_reads_file(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("feat: add feature\nfix: fix bug\n")
f.flush()
lines = lines_from_file(f.name)
os.unlink(f.name)
assert lines == ["feat: add feature", "fix: fix bug"]
def test_skips_blank_lines(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("feat: add feature\n\n\nfix: fix bug\n")
f.flush()
lines = lines_from_file(f.name)
os.unlink(f.name)
assert len(lines) == 2
def test_nonexistent_file_raises(self):
with pytest.raises(CLIError, match="Failed reading"):
lines_from_file("/nonexistent/path.txt")

213
tests/test_dcf_valuation.py Normal file
View File

@@ -0,0 +1,213 @@
"""Unit tests for the DCF Valuation Model."""
import math
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "finance", "financial-analyst", "scripts"
))
from dcf_valuation import DCFModel, safe_divide
class TestSafeDivide:
def test_normal_division(self):
assert safe_divide(10, 2) == 5.0
def test_zero_denominator(self):
assert safe_divide(10, 0) == 0.0
def test_none_denominator(self):
assert safe_divide(10, None) == 0.0
def test_custom_default(self):
assert safe_divide(10, 0, default=-1.0) == -1.0
def test_negative_values(self):
assert safe_divide(-10, 2) == -5.0
@pytest.fixture
def model():
"""A fully configured DCF model with sample data."""
m = DCFModel()
m.set_historical_financials({
"revenue": [80_000_000, 100_000_000],
"net_debt": 20_000_000,
"shares_outstanding": 10_000_000,
})
m.set_assumptions({
"projection_years": 5,
"revenue_growth_rates": [0.15, 0.12, 0.10, 0.08, 0.06],
"fcf_margins": [0.12, 0.13, 0.14, 0.15, 0.16],
"wacc_inputs": {
"risk_free_rate": 0.04,
"equity_risk_premium": 0.06,
"beta": 1.2,
"cost_of_debt": 0.05,
"tax_rate": 0.25,
"equity_weight": 0.70,
"debt_weight": 0.30,
},
"terminal_growth_rate": 0.025,
"exit_ev_ebitda_multiple": 12.0,
"terminal_ebitda_margin": 0.20,
})
return m
class TestWACC:
def test_wacc_calculation(self, model):
wacc = model.calculate_wacc()
# Cost of equity = 0.04 + 1.2 * 0.06 = 0.112
# After-tax cost of debt = 0.05 * (1 - 0.25) = 0.0375
# WACC = 0.70 * 0.112 + 0.30 * 0.0375 = 0.0784 + 0.01125 = 0.08965
assert abs(wacc - 0.08965) < 0.0001
def test_wacc_default_inputs(self):
m = DCFModel()
m.set_assumptions({})
wacc = m.calculate_wacc()
# Defaults: rf=0.04, erp=0.06, beta=1.0, cod=0.05, tax=0.25
# CoE = 0.04 + 1.0 * 0.06 = 0.10
# ATCoD = 0.05 * 0.75 = 0.0375
# WACC = 0.70 * 0.10 + 0.30 * 0.0375 = 0.08125
assert abs(wacc - 0.08125) < 0.0001
class TestProjectCashFlows:
def test_projects_correct_years(self, model):
model.calculate_wacc()
revenue, fcf = model.project_cash_flows()
assert len(revenue) == 5
assert len(fcf) == 5
def test_first_year_revenue(self, model):
model.calculate_wacc()
revenue, _ = model.project_cash_flows()
# base_revenue = 100M, growth = 15%
assert abs(revenue[0] - 115_000_000) < 1
def test_first_year_fcf(self, model):
model.calculate_wacc()
revenue, fcf = model.project_cash_flows()
# Year 1: revenue = 115M, fcf_margin = 12% -> FCF = 13.8M
assert abs(fcf[0] - 13_800_000) < 1
def test_missing_historical_revenue(self):
m = DCFModel()
m.set_historical_financials({})
m.set_assumptions({"projection_years": 3})
with pytest.raises(ValueError, match="Historical revenue"):
m.project_cash_flows()
def test_default_growth_when_rates_short(self):
m = DCFModel()
m.set_historical_financials({"revenue": [100_000]})
m.set_assumptions({
"projection_years": 3,
"revenue_growth_rates": [0.10], # Only 1 year specified
"default_revenue_growth": 0.05,
"fcf_margins": [0.10],
"default_fcf_margin": 0.10,
})
m.calculate_wacc()
revenue, _ = m.project_cash_flows()
assert len(revenue) == 3
# Year 1: 100000 * 1.10 = 110000
# Year 2: 110000 * 1.05 = 115500 (uses default)
assert abs(revenue[1] - 115500) < 1
class TestTerminalValue:
def test_perpetuity_method(self, model):
model.calculate_wacc()
model.project_cash_flows()
tv_perp, tv_exit = model.calculate_terminal_value()
assert tv_perp > 0
def test_exit_multiple_method(self, model):
model.calculate_wacc()
model.project_cash_flows()
_, tv_exit = model.calculate_terminal_value()
# Terminal revenue * ebitda_margin * exit_multiple
terminal_revenue = model.projected_revenue[-1]
expected = terminal_revenue * 0.20 * 12.0
assert abs(tv_exit - expected) < 1
def test_perpetuity_zero_when_wacc_lte_growth(self):
m = DCFModel()
m.set_historical_financials({"revenue": [100_000]})
m.set_assumptions({
"projection_years": 1,
"revenue_growth_rates": [0.05],
"fcf_margins": [0.10],
"terminal_growth_rate": 0.10, # Higher than WACC
"exit_ev_ebitda_multiple": 10.0,
"terminal_ebitda_margin": 0.20,
})
m.wacc = 0.08 # Lower than terminal growth
m.project_cash_flows()
tv_perp, _ = m.calculate_terminal_value()
assert tv_perp == 0.0
class TestEnterpriseAndEquityValue:
def test_full_valuation_pipeline(self, model):
results = model.run_full_valuation()
assert results["wacc"] > 0
assert len(results["projected_revenue"]) == 5
assert results["enterprise_value"]["perpetuity_growth"] > 0
assert results["enterprise_value"]["exit_multiple"] > 0
assert results["equity_value"]["perpetuity_growth"] > 0
assert results["value_per_share"]["perpetuity_growth"] > 0
def test_equity_subtracts_net_debt(self, model):
model.calculate_wacc()
model.project_cash_flows()
model.calculate_terminal_value()
model.calculate_enterprise_value()
model.calculate_equity_value()
# equity = enterprise - net_debt (20M)
assert abs(
model.equity_value_perpetuity -
(model.enterprise_value_perpetuity - 20_000_000)
) < 1
def test_value_per_share(self, model):
model.calculate_wacc()
model.project_cash_flows()
model.calculate_terminal_value()
model.calculate_enterprise_value()
model.calculate_equity_value()
# shares = 10M
expected = model.equity_value_perpetuity / 10_000_000
assert abs(model.value_per_share_perpetuity - expected) < 0.01
class TestSensitivityAnalysis:
def test_returns_table_structure(self, model):
model.calculate_wacc()
model.project_cash_flows()
model.calculate_terminal_value()
result = model.sensitivity_analysis()
assert "wacc_values" in result
assert "growth_values" in result
assert "enterprise_value_table" in result
assert "share_price_table" in result
assert len(result["enterprise_value_table"]) == 5
assert len(result["enterprise_value_table"][0]) == 5
def test_inf_when_wacc_lte_growth(self, model):
model.calculate_wacc()
model.project_cash_flows()
model.calculate_terminal_value()
# Use a growth range that includes values >= wacc
result = model.sensitivity_analysis(
wacc_range=[0.05],
growth_range=[0.05, 0.06],
)
assert result["enterprise_value_table"][0][0] == float("inf")
assert result["enterprise_value_table"][0][1] == float("inf")

View File

@@ -0,0 +1,101 @@
"""Unit tests for the Funnel Analyzer."""
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "marketing-skill", "campaign-analytics", "scripts"
))
from funnel_analyzer import analyze_funnel, compare_segments, safe_divide
class TestAnalyzeFunnel:
def test_basic_funnel(self):
stages = ["Visit", "Signup", "Activate", "Pay"]
counts = [10000, 5000, 2000, 500]
result = analyze_funnel(stages, counts)
assert result["total_entries"] == 10000
assert result["total_conversions"] == 500
assert result["total_lost"] == 9500
assert result["overall_conversion_rate"] == 5.0
def test_stage_metrics_count(self):
stages = ["A", "B", "C"]
counts = [1000, 500, 100]
result = analyze_funnel(stages, counts)
assert len(result["stage_metrics"]) == 3
def test_conversion_rates(self):
stages = ["Visit", "Signup", "Pay"]
counts = [1000, 500, 250]
result = analyze_funnel(stages, counts)
# Visit -> Signup: 500/1000 = 50%
assert result["stage_metrics"][1]["conversion_rate"] == 50.0
# Signup -> Pay: 250/500 = 50%
assert result["stage_metrics"][2]["conversion_rate"] == 50.0
def test_dropoff_detection(self):
stages = ["A", "B", "C"]
counts = [1000, 200, 100]
result = analyze_funnel(stages, counts)
# Biggest absolute drop: A->B (800)
assert result["bottleneck_absolute"]["dropoff_count"] == 800
assert "A -> B" in result["bottleneck_absolute"]["transition"]
def test_relative_bottleneck(self):
stages = ["A", "B", "C"]
counts = [1000, 900, 100]
result = analyze_funnel(stages, counts)
# A->B: dropoff_rate = 10%, B->C: dropoff_rate = 88.89%
assert "B -> C" in result["bottleneck_relative"]["transition"]
def test_cumulative_conversion(self):
stages = ["A", "B", "C"]
counts = [1000, 500, 200]
result = analyze_funnel(stages, counts)
assert result["stage_metrics"][0]["cumulative_conversion"] == 100.0
assert result["stage_metrics"][1]["cumulative_conversion"] == 50.0
assert result["stage_metrics"][2]["cumulative_conversion"] == 20.0
def test_single_stage(self):
result = analyze_funnel(["Only"], [500])
assert result["overall_conversion_rate"] == 100.0
assert result["total_entries"] == 500
assert result["total_lost"] == 0
def test_mismatched_lengths_raises(self):
with pytest.raises(ValueError, match="must match"):
analyze_funnel(["A", "B"], [100])
def test_empty_stages_raises(self):
with pytest.raises(ValueError, match="at least one"):
analyze_funnel([], [])
def test_no_dropoff(self):
stages = ["A", "B"]
counts = [100, 100]
result = analyze_funnel(stages, counts)
assert result["stage_metrics"][1]["conversion_rate"] == 100.0
assert result["stage_metrics"][1]["dropoff_count"] == 0
class TestCompareSegments:
def test_ranks_segments(self):
stages = ["Visit", "Signup", "Pay"]
segments = {
"mobile": {"counts": [1000, 300, 50]},
"desktop": {"counts": [1000, 600, 200]},
}
result = compare_segments(segments, stages)
# Desktop has better overall conversion (20% vs 5%)
assert result["rankings"][0]["segment"] == "desktop"
def test_mismatched_segment_counts_raises(self):
with pytest.raises(ValueError, match="counts"):
compare_segments({"bad": {"counts": [100, 50]}}, ["A", "B", "C"])

View File

@@ -0,0 +1,133 @@
"""Unit tests for the GDPR Compliance Checker."""
import os
import sys
import tempfile
from pathlib import Path
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "ra-qm-team", "gdpr-dsgvo-expert", "scripts"
))
from gdpr_compliance_checker import (
PERSONAL_DATA_PATTERNS,
CODE_PATTERNS,
should_skip,
scan_file_for_patterns,
analyze_project,
)
class TestShouldSkip:
def test_skips_node_modules(self):
assert should_skip(Path("project/node_modules/package/index.js")) is True
def test_skips_venv(self):
assert should_skip(Path("project/venv/lib/site-packages/foo.py")) is True
def test_skips_git(self):
assert should_skip(Path("project/.git/objects/abc123")) is True
def test_allows_normal_path(self):
assert should_skip(Path("project/src/main.py")) is False
def test_allows_deep_path(self):
assert should_skip(Path("project/src/utils/helpers/data.py")) is False
class TestScanFileForPatterns:
def test_detects_email(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write('user_email = "john@example.com"\n')
f.flush()
findings = scan_file_for_patterns(Path(f.name), PERSONAL_DATA_PATTERNS)
os.unlink(f.name)
email_findings = [f for f in findings if f["pattern"] == "email"]
assert len(email_findings) >= 1
assert email_findings[0]["category"] == "contact_data"
def test_detects_health_data(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write('record = {"diagnosis": "flu", "treatment": "rest"}\n')
f.flush()
findings = scan_file_for_patterns(Path(f.name), PERSONAL_DATA_PATTERNS)
os.unlink(f.name)
health_findings = [f for f in findings if f["pattern"] == "health_data"]
assert len(health_findings) >= 1
assert health_findings[0]["risk"] == "critical"
def test_detects_code_logging_issue(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write('log.info("User email: " + user.email)\n')
f.flush()
findings = scan_file_for_patterns(Path(f.name), CODE_PATTERNS)
os.unlink(f.name)
log_findings = [f for f in findings if f["pattern"] == "logging_personal_data"]
assert len(log_findings) >= 1
def test_no_findings_on_clean_file(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write('x = 1 + 2\nprint("hello")\n')
f.flush()
findings = scan_file_for_patterns(Path(f.name), PERSONAL_DATA_PATTERNS)
os.unlink(f.name)
assert len(findings) == 0
def test_handles_unreadable_file(self):
findings = scan_file_for_patterns(Path("/nonexistent/file.py"), PERSONAL_DATA_PATTERNS)
assert findings == []
class TestAnalyzeProject:
def test_scores_clean_project(self):
with tempfile.TemporaryDirectory() as tmpdir:
# Create a clean Python file
src = Path(tmpdir) / "clean.py"
src.write_text("x = 1\ny = 2\nresult = x + y\n", encoding="utf-8")
result = analyze_project(Path(tmpdir))
assert result["summary"]["compliance_score"] == 100
assert result["summary"]["status"] == "compliant"
def test_detects_issues_in_project(self):
with tempfile.TemporaryDirectory() as tmpdir:
src = Path(tmpdir) / "bad.py"
src.write_text(
'user_email = "john@example.com"\n'
'log.info("Patient diagnosis: " + record.diagnosis)\n',
encoding="utf-8",
)
result = analyze_project(Path(tmpdir))
assert result["summary"]["compliance_score"] < 100
assert len(result["personal_data_findings"]) > 0
def test_returns_recommendations(self):
with tempfile.TemporaryDirectory() as tmpdir:
src = Path(tmpdir) / "issues.py"
src.write_text(
'password = "secret123"\n'
'user_email = "test@test.com"\n',
encoding="utf-8",
)
result = analyze_project(Path(tmpdir))
assert "recommendations" in result
assert isinstance(result["recommendations"], list)
class TestPersonalDataPatterns:
"""Test that the regex patterns work correctly."""
@pytest.mark.parametrize("pattern_name,test_string", [
("email", "contact: user@example.com"),
("ip_address", "server IP: 192.168.1.100"),
("phone_number", "call +1-555-123-4567"),
("credit_card", "card: 4111-1111-1111-1111"),
("date_of_birth", "field: date of birth"),
("health_data", "the patient reported symptoms"),
("biometric", "store fingerprint data"),
("religion", "religious preference recorded"),
])
def test_pattern_matches(self, pattern_name, test_string):
import re
pattern = PERSONAL_DATA_PATTERNS[pattern_name]["pattern"]
assert re.search(pattern, test_string, re.IGNORECASE) is not None

176
tests/test_generate_docs.py Normal file
View File

@@ -0,0 +1,176 @@
"""Unit tests for the generate-docs.py infrastructure script."""
import os
import sys
import tempfile
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
# The script uses a hyphenated filename, so import via importlib
import importlib.util
spec = importlib.util.spec_from_file_location(
"generate_docs",
os.path.join(os.path.dirname(__file__), "..", "scripts", "generate-docs.py"),
)
generate_docs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(generate_docs)
class TestSlugify:
def test_basic(self):
assert generate_docs.slugify("my-skill-name") == "my-skill-name"
def test_uppercase(self):
assert generate_docs.slugify("My Skill") == "my-skill"
def test_special_chars(self):
assert generate_docs.slugify("skill_v2.0") == "skill-v2-0"
def test_strips_leading_trailing(self):
assert generate_docs.slugify("--test--") == "test"
class TestPrettify:
def test_kebab_case(self):
assert generate_docs.prettify("senior-backend") == "Senior Backend"
def test_single_word(self):
assert generate_docs.prettify("security") == "Security"
class TestStripContent:
def test_strips_frontmatter(self):
content = "---\nname: test\n---\n# Title\nBody text"
result = generate_docs.strip_content(content)
assert "name: test" not in result
assert "Body text" in result
def test_strips_first_h1(self):
content = "# My Title\nBody text\n# Another H1"
result = generate_docs.strip_content(content)
assert "My Title" not in result
assert "Body text" in result
assert "Another H1" in result
def test_strips_hr_after_title(self):
content = "# Title\n---\nBody text"
result = generate_docs.strip_content(content)
assert result.strip() == "Body text"
def test_no_frontmatter(self):
content = "# Title\nBody text"
result = generate_docs.strip_content(content)
assert "Body text" in result
class TestExtractTitle:
def test_extracts_h1(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("# My Great Skill\nSome content")
f.flush()
title = generate_docs.extract_title(f.name)
os.unlink(f.name)
assert title == "My Great Skill"
def test_skips_frontmatter(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("---\nname: test\n---\n# Real Title\nContent")
f.flush()
title = generate_docs.extract_title(f.name)
os.unlink(f.name)
assert title == "Real Title"
def test_no_h1(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("No heading here\nJust content")
f.flush()
title = generate_docs.extract_title(f.name)
os.unlink(f.name)
assert title is None
def test_nonexistent_file(self):
assert generate_docs.extract_title("/nonexistent/path.md") is None
class TestExtractDescriptionFromFrontmatter:
def test_double_quoted(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write('---\nname: test\ndescription: "My skill description"\n---\n# Title')
f.flush()
desc = generate_docs.extract_description_from_frontmatter(f.name)
os.unlink(f.name)
assert desc == "My skill description"
def test_single_quoted(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("---\nname: test\ndescription: 'Single quoted'\n---\n# Title")
f.flush()
desc = generate_docs.extract_description_from_frontmatter(f.name)
os.unlink(f.name)
assert desc == "Single quoted"
def test_unquoted(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("---\nname: test\ndescription: Unquoted description here\n---\n# Title")
f.flush()
desc = generate_docs.extract_description_from_frontmatter(f.name)
os.unlink(f.name)
assert desc == "Unquoted description here"
def test_no_frontmatter(self):
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as f:
f.write("# Just a title\nNo frontmatter")
f.flush()
desc = generate_docs.extract_description_from_frontmatter(f.name)
os.unlink(f.name)
assert desc is None
class TestFindSkillFiles:
def test_returns_dict(self):
skills = generate_docs.find_skill_files()
assert isinstance(skills, dict)
def test_finds_known_domains(self):
skills = generate_docs.find_skill_files()
# At minimum these domains should have skills
assert "engineering-team" in skills
assert "product-team" in skills
assert "finance" in skills
def test_skips_sample_skills(self):
skills = generate_docs.find_skill_files()
for domain, skill_list in skills.items():
for skill in skill_list:
assert "assets/sample-skill" not in skill["rel_path"]
class TestRewriteSkillInternalLinks:
def test_rewrites_script_link(self):
content = "[my script](scripts/calculator.py)"
result = generate_docs.rewrite_skill_internal_links(content, "product-team/my-skill")
assert "github.com" in result
assert "product-team/my-skill/scripts/calculator.py" in result
def test_preserves_external_links(self):
content = "[Google](https://google.com)"
result = generate_docs.rewrite_skill_internal_links(content, "product-team/my-skill")
assert result == content
def test_preserves_anchor_links(self):
content = "[section](#my-section)"
result = generate_docs.rewrite_skill_internal_links(content, "product-team/my-skill")
assert result == content
class TestDomainMapping:
def test_all_domains_have_sort_order(self):
for key, value in generate_docs.DOMAINS.items():
assert len(value) == 4
assert isinstance(value[1], int)
def test_unique_sort_orders(self):
orders = [v[1] for v in generate_docs.DOMAINS.values()]
assert len(orders) == len(set(orders))

128
tests/test_okr_tracker.py Normal file
View File

@@ -0,0 +1,128 @@
"""Unit tests for the OKR Tracker."""
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "c-level-advisor", "coo-advisor", "scripts"
))
from okr_tracker import calculate_kr_score, get_kr_status
class TestCalculateKrScoreNumeric:
def test_basic_numeric(self):
kr = {"type": "numeric", "baseline_value": 0, "current_value": 50, "target_value": 100}
assert calculate_kr_score(kr) == 0.5
def test_at_target(self):
kr = {"type": "numeric", "baseline_value": 0, "current_value": 100, "target_value": 100}
assert calculate_kr_score(kr) == 1.0
def test_no_progress(self):
kr = {"type": "numeric", "baseline_value": 0, "current_value": 0, "target_value": 100}
assert calculate_kr_score(kr) == 0.0
def test_clamped_above_one(self):
kr = {"type": "numeric", "baseline_value": 0, "current_value": 150, "target_value": 100}
assert calculate_kr_score(kr) == 1.0
def test_target_equals_baseline(self):
kr = {"type": "numeric", "baseline_value": 50, "current_value": 50, "target_value": 50}
assert calculate_kr_score(kr) == 0.0
def test_lower_is_better(self):
# Reducing churn from 10% to 5%, currently at 7%
kr = {
"type": "numeric",
"baseline_value": 10,
"current_value": 7,
"target_value": 5,
"lower_is_better": True,
}
# improvement = 10 - 7 = 3, needed = 10 - 5 = 5 -> score = 0.6
assert abs(calculate_kr_score(kr) - 0.6) < 0.01
def test_lower_is_better_at_target(self):
kr = {
"type": "numeric",
"baseline_value": 10,
"current_value": 5,
"target_value": 5,
"lower_is_better": True,
}
assert calculate_kr_score(kr) == 1.0
def test_lower_is_better_exceeded(self):
kr = {
"type": "numeric",
"baseline_value": 10,
"current_value": 3,
"target_value": 5,
"lower_is_better": True,
}
assert calculate_kr_score(kr) == 1.0
class TestCalculateKrScorePercentage:
def test_percentage_midway(self):
kr = {"type": "percentage", "baseline_pct": 10, "current_pct": 15, "target_pct": 20}
assert calculate_kr_score(kr) == 0.5
def test_percentage_at_target(self):
kr = {"type": "percentage", "baseline_pct": 0, "current_pct": 100, "target_pct": 100}
assert calculate_kr_score(kr) == 1.0
def test_percentage_target_equals_baseline(self):
kr = {"type": "percentage", "baseline_pct": 50, "current_pct": 50, "target_pct": 50}
assert calculate_kr_score(kr) == 0.0
class TestCalculateKrScoreMilestone:
def test_milestone_explicit_score(self):
kr = {"type": "milestone", "score": 0.75}
assert calculate_kr_score(kr) == 0.75
def test_milestone_hit_count(self):
kr = {"type": "milestone", "milestones_total": 4, "milestones_hit": 3}
assert calculate_kr_score(kr) == 0.75
def test_milestone_clamped(self):
kr = {"type": "milestone", "score": 1.5}
assert calculate_kr_score(kr) == 1.0
class TestCalculateKrScoreBoolean:
def test_boolean_done(self):
kr = {"type": "boolean", "done": True}
assert calculate_kr_score(kr) == 1.0
def test_boolean_not_done(self):
kr = {"type": "boolean", "done": False}
assert calculate_kr_score(kr) == 0.0
class TestGetKrStatus:
def test_on_track(self):
status = get_kr_status(0.8, 0.5, {})
assert status == "on_track"
def test_complete_requires_done_flag(self):
# "complete" status requires kr["done"] = True
status = get_kr_status(1.0, 0.5, {"done": True})
assert status == "complete"
def test_score_one_without_done_is_on_track(self):
status = get_kr_status(1.0, 0.5, {})
assert status == "on_track"
def test_not_started(self):
# not_started requires score==0 AND quarter_progress < 0.1
status = get_kr_status(0.0, 0.05, {})
assert status == "not_started"
def test_off_track(self):
# Very low score deep into the quarter
status = get_kr_status(0.1, 0.8, {})
assert status == "off_track"

View File

@@ -0,0 +1,194 @@
"""Unit tests for the Financial Ratio Calculator."""
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "finance", "financial-analyst", "scripts"
))
from ratio_calculator import FinancialRatioCalculator, safe_divide
@pytest.fixture
def sample_data():
return {
"income_statement": {
"revenue": 1_000_000,
"cost_of_goods_sold": 400_000,
"operating_income": 200_000,
"net_income": 150_000,
"interest_expense": 20_000,
"ebitda": 250_000,
},
"balance_sheet": {
"total_assets": 2_000_000,
"total_equity": 1_200_000,
"current_assets": 500_000,
"current_liabilities": 300_000,
"inventory": 100_000,
"cash_and_equivalents": 200_000,
"total_debt": 500_000,
"accounts_receivable": 150_000,
},
"cash_flow": {
"operating_cash_flow": 180_000,
},
"market_data": {
"share_price": 50.0,
"shares_outstanding": 100_000,
"earnings_growth_rate": 0.15,
},
}
@pytest.fixture
def calc(sample_data):
return FinancialRatioCalculator(sample_data)
class TestProfitability:
def test_roe(self, calc):
ratios = calc.calculate_profitability()
# 150000 / 1200000 = 0.125
assert abs(ratios["roe"]["value"] - 0.125) < 0.001
def test_roa(self, calc):
ratios = calc.calculate_profitability()
# 150000 / 2000000 = 0.075
assert abs(ratios["roa"]["value"] - 0.075) < 0.001
def test_gross_margin(self, calc):
ratios = calc.calculate_profitability()
# (1000000 - 400000) / 1000000 = 0.60
assert abs(ratios["gross_margin"]["value"] - 0.60) < 0.001
def test_operating_margin(self, calc):
ratios = calc.calculate_profitability()
# 200000 / 1000000 = 0.20
assert abs(ratios["operating_margin"]["value"] - 0.20) < 0.001
def test_net_margin(self, calc):
ratios = calc.calculate_profitability()
# 150000 / 1000000 = 0.15
assert abs(ratios["net_margin"]["value"] - 0.15) < 0.001
def test_interpretation_populated(self, calc):
ratios = calc.calculate_profitability()
for key in ratios:
assert "interpretation" in ratios[key]
class TestLiquidity:
def test_current_ratio(self, calc):
ratios = calc.calculate_liquidity()
# 500000 / 300000 = 1.667
assert abs(ratios["current_ratio"]["value"] - 1.667) < 0.01
def test_quick_ratio(self, calc):
ratios = calc.calculate_liquidity()
# (500000 - 100000) / 300000 = 1.333
assert abs(ratios["quick_ratio"]["value"] - 1.333) < 0.01
def test_cash_ratio(self, calc):
ratios = calc.calculate_liquidity()
# 200000 / 300000 = 0.667
assert abs(ratios["cash_ratio"]["value"] - 0.667) < 0.01
class TestLeverage:
def test_debt_to_equity(self, calc):
ratios = calc.calculate_leverage()
# 500000 / 1200000 = 0.417
assert abs(ratios["debt_to_equity"]["value"] - 0.417) < 0.01
def test_interest_coverage(self, calc):
ratios = calc.calculate_leverage()
# 200000 / 20000 = 10.0
assert abs(ratios["interest_coverage"]["value"] - 10.0) < 0.01
class TestEfficiency:
def test_asset_turnover(self, calc):
ratios = calc.calculate_efficiency()
# 1000000 / 2000000 = 0.5
assert abs(ratios["asset_turnover"]["value"] - 0.5) < 0.01
def test_inventory_turnover(self, calc):
ratios = calc.calculate_efficiency()
# 400000 / 100000 = 4.0
assert abs(ratios["inventory_turnover"]["value"] - 4.0) < 0.01
def test_dso(self, calc):
ratios = calc.calculate_efficiency()
# receivables_turnover = 1000000 / 150000 = 6.667
# DSO = 365 / 6.667 = 54.75
assert abs(ratios["dso"]["value"] - 54.75) < 0.5
class TestValuation:
def test_pe_ratio(self, calc):
ratios = calc.calculate_valuation()
# EPS = 150000 / 100000 = 1.5
# PE = 50.0 / 1.5 = 33.33
assert abs(ratios["pe_ratio"]["value"] - 33.33) < 0.1
def test_ev_ebitda(self, calc):
ratios = calc.calculate_valuation()
# market_cap = 50 * 100000 = 5000000
# EV = 5000000 + 500000 - 200000 = 5300000
# EV/EBITDA = 5300000 / 250000 = 21.2
assert abs(ratios["ev_ebitda"]["value"] - 21.2) < 0.1
class TestCalculateAll:
def test_returns_all_categories(self, calc):
results = calc.calculate_all()
assert "profitability" in results
assert "liquidity" in results
assert "leverage" in results
assert "efficiency" in results
assert "valuation" in results
class TestInterpretation:
def test_dso_lower_is_better(self, calc):
result = calc.interpret_ratio("dso", 25.0)
assert "Excellent" in result
def test_dso_high_is_concern(self, calc):
result = calc.interpret_ratio("dso", 90.0)
assert "Concern" in result
def test_debt_to_equity_conservative(self, calc):
result = calc.interpret_ratio("debt_to_equity", 0.2)
assert "Conservative" in result
def test_zero_value(self, calc):
result = calc.interpret_ratio("roe", 0.0)
assert "Insufficient" in result
def test_unknown_ratio(self, calc):
result = calc.interpret_ratio("unknown_ratio", 5.0)
assert "No benchmark" in result
class TestEdgeCases:
def test_zero_revenue(self):
data = {"income_statement": {"revenue": 0}, "balance_sheet": {}, "cash_flow": {}, "market_data": {}}
calc = FinancialRatioCalculator(data)
ratios = calc.calculate_profitability()
assert ratios["gross_margin"]["value"] == 0.0
def test_zero_equity(self):
data = {"income_statement": {"net_income": 100}, "balance_sheet": {"total_equity": 0}, "cash_flow": {}, "market_data": {}}
calc = FinancialRatioCalculator(data)
ratios = calc.calculate_profitability()
assert ratios["roe"]["value"] == 0.0
def test_missing_market_data(self):
data = {"income_statement": {}, "balance_sheet": {}, "cash_flow": {}, "market_data": {}}
calc = FinancialRatioCalculator(data)
ratios = calc.calculate_valuation()
assert ratios["pe_ratio"]["value"] == 0.0

View File

@@ -0,0 +1,143 @@
"""Unit tests for the RICE Prioritizer."""
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "product-team", "product-manager-toolkit", "scripts"
))
from rice_prioritizer import RICECalculator
@pytest.fixture
def calc():
return RICECalculator()
class TestCalculateRice:
"""Test the core RICE formula: (Reach * Impact * Confidence) / Effort."""
def test_basic_calculation(self, calc):
# reach=1000, impact=high(2.0), confidence=high(100/100=1.0), effort=m(5)
# = (1000 * 2.0 * 1.0) / 5 = 400.0
assert calc.calculate_rice(1000, "high", "high", "m") == 400.0
def test_massive_impact(self, calc):
# reach=500, impact=massive(3.0), confidence=medium(0.8), effort=s(3)
# = (500 * 3.0 * 0.8) / 3 = 400.0
assert calc.calculate_rice(500, "massive", "medium", "s") == 400.0
def test_minimal_impact(self, calc):
# reach=1000, impact=minimal(0.25), confidence=low(0.5), effort=xs(1)
# = (1000 * 0.25 * 0.5) / 1 = 125.0
assert calc.calculate_rice(1000, "minimal", "low", "xs") == 125.0
def test_zero_reach(self, calc):
assert calc.calculate_rice(0, "high", "high", "m") == 0.0
def test_case_insensitive(self, calc):
assert calc.calculate_rice(1000, "HIGH", "HIGH", "M") == 400.0
def test_unknown_impact_defaults_to_one(self, calc):
# Unknown impact maps to 1.0
# reach=1000, impact=1.0, confidence=high(1.0), effort=m(5)
# = (1000 * 1.0 * 1.0) / 5 = 200.0
assert calc.calculate_rice(1000, "unknown", "high", "m") == 200.0
def test_xl_effort(self, calc):
# reach=1300, impact=medium(1.0), confidence=high(1.0), effort=xl(13)
# = (1300 * 1.0 * 1.0) / 13 = 100.0
assert calc.calculate_rice(1300, "medium", "high", "xl") == 100.0
@pytest.mark.parametrize("impact,expected_score", [
("massive", 3.0),
("high", 2.0),
("medium", 1.0),
("low", 0.5),
("minimal", 0.25),
])
def test_impact_map(self, calc, impact, expected_score):
# reach=100, confidence=high(1.0), effort=xs(1) -> score = 100 * impact
result = calc.calculate_rice(100, impact, "high", "xs")
assert result == round(100 * expected_score, 2)
class TestPrioritizeFeatures:
"""Test feature sorting by RICE score."""
def test_sorts_descending(self, calc):
features = [
{"name": "low", "reach": 100, "impact": "low", "confidence": "low", "effort": "xl"},
{"name": "high", "reach": 10000, "impact": "massive", "confidence": "high", "effort": "xs"},
]
result = calc.prioritize_features(features)
assert result[0]["name"] == "high"
assert result[1]["name"] == "low"
def test_adds_rice_score(self, calc):
features = [{"name": "test", "reach": 1000, "impact": "high", "confidence": "high", "effort": "m"}]
result = calc.prioritize_features(features)
assert "rice_score" in result[0]
assert result[0]["rice_score"] == 400.0
def test_empty_list(self, calc):
assert calc.prioritize_features([]) == []
def test_defaults_for_missing_fields(self, calc):
features = [{"name": "sparse"}]
result = calc.prioritize_features(features)
assert result[0]["rice_score"] == 0.0 # reach defaults to 0
class TestAnalyzePortfolio:
"""Test portfolio analysis metrics."""
def test_empty_features(self, calc):
assert calc.analyze_portfolio([]) == {}
def test_counts_quick_wins(self, calc):
features = [
{"name": "qw", "reach": 1000, "impact": "high", "confidence": "high", "effort": "xs", "rice_score": 100},
{"name": "big", "reach": 1000, "impact": "high", "confidence": "high", "effort": "xl", "rice_score": 50},
]
result = calc.analyze_portfolio(features)
assert result["quick_wins"] == 1
assert result["big_bets"] == 1
assert result["total_features"] == 2
def test_total_effort(self, calc):
features = [
{"name": "a", "effort": "m", "rice_score": 10}, # 5 months
{"name": "b", "effort": "s", "rice_score": 20}, # 3 months
]
result = calc.analyze_portfolio(features)
assert result["total_effort_months"] == 8
class TestGenerateRoadmap:
"""Test roadmap generation with capacity constraints."""
def test_single_quarter(self, calc):
features = [
{"name": "a", "effort": "s", "rice_score": 100}, # 3 months
{"name": "b", "effort": "s", "rice_score": 50}, # 3 months
]
roadmap = calc.generate_roadmap(features, team_capacity=10)
assert len(roadmap) == 1
assert len(roadmap[0]["features"]) == 2
assert roadmap[0]["capacity_used"] == 6
def test_overflow_to_next_quarter(self, calc):
features = [
{"name": "a", "effort": "l", "rice_score": 100}, # 8 months
{"name": "b", "effort": "l", "rice_score": 50}, # 8 months
]
roadmap = calc.generate_roadmap(features, team_capacity=10)
assert len(roadmap) == 2
assert roadmap[0]["features"][0]["name"] == "a"
assert roadmap[1]["features"][0]["name"] == "b"
def test_empty_features(self, calc):
assert calc.generate_roadmap([], team_capacity=10) == []

167
tests/test_seo_checker.py Normal file
View File

@@ -0,0 +1,167 @@
"""Unit tests for the SEO Checker."""
import sys
import os
import pytest
sys.path.insert(0, os.path.join(
os.path.dirname(__file__), "..", "marketing-skill", "seo-audit", "scripts"
))
from seo_checker import SEOParser, analyze_html, compute_overall_score
class TestSEOParser:
def test_extracts_title(self):
p = SEOParser()
p.feed("<html><head><title>My Page Title</title></head></html>")
assert p.title == "My Page Title"
def test_extracts_meta_description(self):
p = SEOParser()
p.feed('<html><head><meta name="description" content="A great page"></head></html>')
assert p.meta_description == "A great page"
def test_extracts_og_description_fallback(self):
p = SEOParser()
p.feed('<html><head><meta property="og:description" content="OG desc"></head></html>')
assert p.meta_description == "OG desc"
def test_meta_description_takes_priority_over_og(self):
p = SEOParser()
p.feed('<head><meta name="description" content="Primary"><meta property="og:description" content="OG"></head>')
assert p.meta_description == "Primary"
def test_extracts_headings(self):
p = SEOParser()
p.feed("<h1>Main Title</h1><h2>Section 1</h2><h3>Subsection</h3>")
assert len(p.h_tags) == 3
assert p.h_tags[0] == (1, "Main Title")
assert p.h_tags[1] == (2, "Section 1")
assert p.h_tags[2] == (3, "Subsection")
def test_extracts_images(self):
p = SEOParser()
p.feed('<img src="photo.jpg" alt="A photo"><img src="icon.png">')
assert len(p.images) == 2
assert p.images[0]["alt"] == "A photo"
assert p.images[1]["alt"] is None
def test_extracts_links(self):
p = SEOParser()
p.feed('<a href="/internal">Click here</a><a href="https://example.com">External</a>')
assert len(p.links) == 2
assert p.links[0]["href"] == "/internal"
assert p.links[1]["href"] == "https://example.com"
def test_viewport_meta(self):
p = SEOParser()
p.feed('<meta name="viewport" content="width=device-width">')
assert p.viewport_meta is True
def test_ignores_script_content(self):
p = SEOParser()
p.feed("<body><script>var x = 1;</script><p>Real content</p></body>")
body_text = " ".join(p.body_text_parts)
assert "var x" not in body_text
assert "Real content" in body_text
class TestAnalyzeHTML:
def test_perfect_title(self):
# 55 chars is within 50-60 optimal range
title = "A" * 55
html = f"<html><head><title>{title}</title></head><body></body></html>"
result = analyze_html(html)
assert result["title"]["pass"] is True
assert result["title"]["score"] == 100
def test_missing_title(self):
result = analyze_html("<html><head></head><body></body></html>")
assert result["title"]["pass"] is False
assert result["title"]["score"] == 0
def test_one_h1_passes(self):
result = analyze_html("<h1>Title</h1>")
assert result["h1"]["pass"] is True
assert result["h1"]["count"] == 1
def test_multiple_h1s_fail(self):
result = analyze_html("<h1>First</h1><h1>Second</h1>")
assert result["h1"]["pass"] is False
assert result["h1"]["count"] == 2
def test_no_h1_fails(self):
result = analyze_html("<h2>No H1</h2>")
assert result["h1"]["pass"] is False
assert result["h1"]["count"] == 0
def test_heading_hierarchy_skip(self):
result = analyze_html("<h1>Title</h1><h3>Skipped H2</h3>")
assert result["heading_hierarchy"]["pass"] is False
assert len(result["heading_hierarchy"]["issues"]) == 1
def test_heading_hierarchy_ok(self):
result = analyze_html("<h1>Title</h1><h2>Section</h2><h3>Sub</h3>")
assert result["heading_hierarchy"]["pass"] is True
def test_image_alt_text_all_present(self):
result = analyze_html('<img src="a.jpg" alt="Photo"><img src="b.jpg" alt="Icon">')
assert result["image_alt_text"]["pass"] is True
assert result["image_alt_text"]["coverage_pct"] == 100.0
def test_image_alt_text_missing(self):
result = analyze_html('<img src="a.jpg" alt="Photo"><img src="b.jpg">')
assert result["image_alt_text"]["pass"] is False
assert result["image_alt_text"]["with_alt"] == 1
def test_no_images_passes(self):
result = analyze_html("<p>No images</p>")
assert result["image_alt_text"]["pass"] is True
def test_word_count_sufficient(self):
words = " ".join(["word"] * 350)
result = analyze_html(f"<body><p>{words}</p></body>")
assert result["word_count"]["pass"] is True
assert result["word_count"]["count"] >= 300
def test_word_count_insufficient(self):
result = analyze_html("<body><p>Too few words here</p></body>")
assert result["word_count"]["pass"] is False
def test_viewport_present(self):
result = analyze_html('<meta name="viewport" content="width=device-width">')
assert result["viewport_meta"]["pass"] is True
def test_viewport_missing(self):
result = analyze_html("<html><head></head></html>")
assert result["viewport_meta"]["pass"] is False
class TestComputeOverallScore:
def test_returns_integer(self):
html = "<html><head><title>Test</title></head><body><h1>Title</h1></body></html>"
results = analyze_html(html)
score = compute_overall_score(results)
assert isinstance(score, int)
assert 0 <= score <= 100
def test_demo_html_scores_reasonably(self):
from seo_checker import DEMO_HTML
results = analyze_html(DEMO_HTML)
score = compute_overall_score(results)
# Demo page is well-optimized, should score above 70
assert score >= 70
class TestEdgeCases:
def test_empty_html(self):
result = analyze_html("")
assert result["title"]["pass"] is False
assert result["h1"]["count"] == 0
def test_malformed_html(self):
# Should not crash on malformed HTML
result = analyze_html("<h1>Unclosed<h2>Nested badly")
assert isinstance(result, dict)
assert "h1" in result

View File

@@ -0,0 +1,192 @@
"""Integration tests: verify skill package consistency across the repository.
These tests validate that:
1. Every skill directory with a SKILL.md has valid structure
2. SKILL.md files have required YAML frontmatter
3. File references in SKILL.md actually exist
4. Scripts directories contain valid Python files
5. No orphaned scripts directories without a SKILL.md
"""
import glob
import os
import re
import pytest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SKILL_DOMAINS = [
"engineering-team",
"engineering",
"product-team",
"marketing-skill",
"project-management",
"c-level-advisor",
"ra-qm-team",
"business-growth",
"finance",
]
SKIP_PATTERNS = [
"assets/sample-skill",
"assets/sample_codebase",
"__pycache__",
]
def _find_all_skill_dirs():
"""Find all directories containing a SKILL.md file."""
skills = []
for domain in SKILL_DOMAINS:
domain_path = os.path.join(REPO_ROOT, domain)
if not os.path.isdir(domain_path):
continue
for root, dirs, files in os.walk(domain_path):
if "SKILL.md" in files:
rel = os.path.relpath(root, REPO_ROOT)
if any(skip in rel for skip in SKIP_PATTERNS):
continue
skills.append(root)
return skills
ALL_SKILL_DIRS = _find_all_skill_dirs()
def _short_id(path):
return os.path.relpath(path, REPO_ROOT)
class TestSkillMdExists:
"""Every recognized skill directory must have a SKILL.md."""
def test_found_skills(self):
assert len(ALL_SKILL_DIRS) > 100, f"Expected 100+ skills, found {len(ALL_SKILL_DIRS)}"
class TestSkillMdFrontmatter:
"""SKILL.md files should have YAML frontmatter with name and description."""
@pytest.mark.parametrize(
"skill_dir",
ALL_SKILL_DIRS,
ids=[_short_id(s) for s in ALL_SKILL_DIRS],
)
def test_has_frontmatter(self, skill_dir):
skill_md = os.path.join(skill_dir, "SKILL.md")
with open(skill_md, "r", encoding="utf-8") as f:
content = f.read()
# Check for YAML frontmatter delimiters
assert content.startswith("---"), (
f"{_short_id(skill_dir)}/SKILL.md is missing YAML frontmatter (no opening ---)"
)
# Find closing ---
second_delim = content.find("---", 4)
assert second_delim > 0, (
f"{_short_id(skill_dir)}/SKILL.md has unclosed frontmatter"
)
@pytest.mark.parametrize(
"skill_dir",
ALL_SKILL_DIRS,
ids=[_short_id(s) for s in ALL_SKILL_DIRS],
)
def test_frontmatter_has_name(self, skill_dir):
skill_md = os.path.join(skill_dir, "SKILL.md")
with open(skill_md, "r", encoding="utf-8") as f:
content = f.read()
match = re.match(r"^---\n(.*?)---\n", content, re.DOTALL)
if match:
fm = match.group(1)
assert "name:" in fm, (
f"{_short_id(skill_dir)}/SKILL.md frontmatter missing 'name' field"
)
class TestSkillMdHasH1:
"""Every SKILL.md must have at least one H1 heading."""
@pytest.mark.parametrize(
"skill_dir",
ALL_SKILL_DIRS,
ids=[_short_id(s) for s in ALL_SKILL_DIRS],
)
def test_has_h1(self, skill_dir):
skill_md = os.path.join(skill_dir, "SKILL.md")
with open(skill_md, "r", encoding="utf-8") as f:
content = f.read()
# Strip frontmatter
content = re.sub(r"^---\n.*?---\n", "", content, flags=re.DOTALL)
assert re.search(r"^# .+", content, re.MULTILINE), (
f"{_short_id(skill_dir)}/SKILL.md has no H1 heading"
)
class TestScriptDirectories:
"""Validate scripts/ directories within skills."""
def _get_skills_with_scripts(self):
result = []
for skill_dir in ALL_SKILL_DIRS:
scripts_dir = os.path.join(skill_dir, "scripts")
if os.path.isdir(scripts_dir):
py_files = glob.glob(os.path.join(scripts_dir, "*.py"))
if py_files:
result.append((skill_dir, py_files))
return result
def test_scripts_dirs_have_python_files(self):
"""Every scripts/ directory should contain at least one .py file."""
for skill_dir in ALL_SKILL_DIRS:
scripts_dir = os.path.join(skill_dir, "scripts")
if os.path.isdir(scripts_dir):
py_files = glob.glob(os.path.join(scripts_dir, "*.py"))
assert len(py_files) > 0, (
f"{_short_id(skill_dir)}/scripts/ exists but has no .py files"
)
def test_no_empty_skill_md(self):
"""SKILL.md files should not be empty."""
for skill_dir in ALL_SKILL_DIRS:
skill_md = os.path.join(skill_dir, "SKILL.md")
size = os.path.getsize(skill_md)
assert size > 100, (
f"{_short_id(skill_dir)}/SKILL.md is suspiciously small ({size} bytes)"
)
class TestReferencesDirectories:
"""Validate references/ directories are non-empty."""
def test_references_not_empty(self):
for skill_dir in ALL_SKILL_DIRS:
refs_dir = os.path.join(skill_dir, "references")
if os.path.isdir(refs_dir):
files = [f for f in os.listdir(refs_dir) if not f.startswith(".")]
assert len(files) > 0, (
f"{_short_id(skill_dir)}/references/ exists but is empty"
)
class TestNoDuplicateSkillNames:
"""Skill directory names should be unique across the entire repo."""
def test_unique_top_level_skill_names(self):
"""Top-level skills (direct children of domains) should not have 3+ duplicates."""
names = {}
for skill_dir in ALL_SKILL_DIRS:
rel = _short_id(skill_dir)
parts = rel.split(os.sep)
# Only check top-level skills (domain/skill-name), not sub-skills
if len(parts) != 2:
continue
name = parts[1]
names.setdefault(name, []).append(rel)
# Report names that appear 3+ times (2 is acceptable for cross-domain)
triples = {k: v for k, v in names.items() if len(v) >= 3}
assert not triples, f"Top-level skill names appearing 3+ times: {triples}"

90
tests/test_smoke.py Normal file
View File

@@ -0,0 +1,90 @@
"""Smoke tests: syntax compilation and --help for all Python scripts.
These tests verify that every Python script in the repository:
1. Compiles without syntax errors (all scripts)
2. Runs --help without crashing (argparse-based scripts only)
"""
import glob
import os
import py_compile
import subprocess
import sys
import pytest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Directories to skip (sample/fixture code, not real scripts)
SKIP_PATTERNS = [
"assets/sample_codebase",
"__pycache__",
".venv",
"tests/",
]
def _collect_all_python_scripts():
"""Find all .py files in the repo, excluding test/fixture code."""
all_py = glob.glob(os.path.join(REPO_ROOT, "**", "*.py"), recursive=True)
scripts = []
for path in sorted(all_py):
rel = os.path.relpath(path, REPO_ROOT)
if any(skip in rel for skip in SKIP_PATTERNS):
continue
scripts.append(path)
return scripts
def _has_argparse(path):
"""Check if a script imports argparse (heuristic)."""
try:
with open(path, "r", encoding="utf-8", errors="replace") as f:
content = f.read()
return "ArgumentParser" in content or "import argparse" in content
except Exception:
return False
ALL_SCRIPTS = _collect_all_python_scripts()
ARGPARSE_SCRIPTS = [s for s in ALL_SCRIPTS if _has_argparse(s)]
def _short_id(path):
"""Create a readable test ID from a full path."""
return os.path.relpath(path, REPO_ROOT)
class TestSyntaxCompilation:
"""Every Python file must compile without syntax errors."""
@pytest.mark.parametrize(
"script_path",
ALL_SCRIPTS,
ids=[_short_id(s) for s in ALL_SCRIPTS],
)
def test_syntax(self, script_path):
py_compile.compile(script_path, doraise=True)
class TestArgparseHelp:
"""Every argparse-based script must run --help successfully."""
@pytest.mark.parametrize(
"script_path",
ARGPARSE_SCRIPTS,
ids=[_short_id(s) for s in ARGPARSE_SCRIPTS],
)
def test_help_flag(self, script_path):
result = subprocess.run(
[sys.executable, script_path, "--help"],
capture_output=True,
text=True,
timeout=30,
cwd=REPO_ROOT,
)
assert result.returncode == 0, (
f"--help failed for {os.path.relpath(script_path, REPO_ROOT)}:\n"
f"STDOUT: {result.stdout[:500]}\n"
f"STDERR: {result.stderr[:500]}"
)