Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/agentic_orchestrator/translation/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,9 @@ async def translate_to_korean(self, text: str) -> str:

translated = response.content.strip()

# Remove any wrapper text the model might add
if translated.startswith("Korean translation:"):
# Remove any wrapper text the model might add (case-insensitive,
# matching translate_to_english's handling of the prefix).
if translated.lower().startswith("korean translation:"):
translated = translated[len("Korean translation:") :].strip()

# Remove markdown separator markers that the model might add
Expand Down
107 changes: 107 additions & 0 deletions tests/test_plan_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Tests for PlanParser (project/parser.py).

Previously had zero coverage. PlanParser.parse() is synchronous and regex-based
(no LLM), so it can be exercised end-to-end on a representative plan document.
"""

from agentic_orchestrator.project.parser import PlanParser, TechStack

SAMPLE_PLAN = """# Mossland NFT Valuation Tracker

## Project Overview
A real-time dashboard for tracking Mossland NFT valuations using AI agents.
It serves NFT holders who want live, accurate price data and portfolio insights.

## Technical Architecture
Frontend: Next.js with React. Backend: FastAPI (Python). Database: PostgreSQL.
Blockchain: Ethereum with Solidity smart contracts.

## Features
- Real-time NFT price tracking
- Portfolio valuation dashboard
- Price alerts via webhooks

## Risks
- External API rate limits
- On-chain data latency

## KPIs
- 1000 monthly active users
- 99.9% uptime
"""


class TestParseEndToEnd:
def setup_method(self):
self.parser = PlanParser()

def test_title_from_h1(self):
parsed = self.parser.parse(SAMPLE_PLAN)
assert parsed.title == "Mossland NFT Valuation Tracker"

def test_explicit_title_overrides_extraction(self):
parsed = self.parser.parse(SAMPLE_PLAN, title="Explicit Title")
assert parsed.title == "Explicit Title"

def test_tech_stack_detected(self):
ts = self.parser.parse(SAMPLE_PLAN).tech_stack
assert ts.frontend == "nextjs"
assert ts.backend == "fastapi"
assert ts.database == "postgresql"
assert ts.blockchain == "ethereum"
# react also appears but the primary frontend slot is taken -> additional
assert "react" in ts.additional

def test_features_extracted(self):
features = self.parser.parse(SAMPLE_PLAN).features
assert len(features) == 3
assert "Real-time NFT price tracking" in features

def test_risks_and_kpis(self):
parsed = self.parser.parse(SAMPLE_PLAN)
assert len(parsed.risks) == 2
assert len(parsed.kpis) == 2

def test_summary_non_empty(self):
summary = self.parser.parse(SAMPLE_PLAN).summary
assert "real-time dashboard" in summary.lower()

def test_raw_content_preserved(self):
assert self.parser.parse(SAMPLE_PLAN).raw_content == SAMPLE_PLAN


class TestParseEdgeCases:
def setup_method(self):
self.parser = PlanParser()

def test_empty_content_returns_untitled(self):
parsed = self.parser.parse("")
assert parsed.title == "Untitled Project"
assert parsed.features == []
assert isinstance(parsed.tech_stack, TechStack)

def test_empty_content_respects_given_title(self):
assert self.parser.parse("", title="Given").title == "Given"

def test_title_from_plan_prefix_when_no_h1(self):
parsed = self.parser.parse("Plan: Decentralized Identity Vault\n\nSome body text here.")
assert parsed.title == "Decentralized Identity Vault"


class TestTechDetection:
def setup_method(self):
self.parser = PlanParser()

def test_detects_vue_and_django_and_solana(self):
ts = self.parser._detect_tech_stack(
"We will use Vue.js on the frontend, Django backend, and the Solana blockchain."
)
assert ts.frontend == "vue"
assert ts.backend == "django"
assert ts.blockchain == "solana"

def test_no_tech_leaves_fields_empty(self):
ts = self.parser._detect_tech_stack("A plan with no concrete technology mentioned at all.")
assert ts.frontend is None
assert ts.backend is None
assert ts.blockchain is None
125 changes: 125 additions & 0 deletions tests/test_translator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Tests for ContentTranslator (translation/translator.py).

Previously had zero coverage. The LLM call is replaced by a FakeRouter so the
language detection, already-in-target-language short-circuits, model-wrapper
stripping, and bilingual orchestration are exercised deterministically.
"""

import pytest

from agentic_orchestrator.translation.translator import ContentTranslator


class FakeResponse:
def __init__(self, content, model="fake-model"):
self.content = content
self.model = model


class FakeRouter:
"""Records calls and returns a canned translation."""

def __init__(self, reply="TRANSLATED"):
self.reply = reply
self.calls = []

async def route(self, **kwargs):
self.calls.append(kwargs)
return FakeResponse(self.reply)


@pytest.fixture
def translator():
return ContentTranslator(router=FakeRouter())


class TestDetectLanguage:
def test_korean(self, translator):
assert translator._detect_language("안녕하세요 반갑습니다 여러분") == "ko"

def test_english(self, translator):
assert translator._detect_language("Hello world this is English text") == "en"

def test_empty_defaults_to_english(self, translator):
assert translator._detect_language("") == "en"

def test_numbers_and_symbols_only(self, translator):
assert translator._detect_language("12345 !@#$% 67890") == "en"

def test_mostly_english_with_a_little_korean(self, translator):
# 3 Korean chars vs many ASCII letters -> ratio <= 0.3 -> "en"
assert translator._detect_language("Mossland NFT portfolio 트래커") == "en"

def test_mostly_korean(self, translator):
assert translator._detect_language("모스랜드 NFT 가치 평가 대시보드 서비스") == "ko"


class TestTranslateShortCircuits:
async def test_to_english_skips_when_already_english(self, translator):
out = await translator.translate_to_english("This text is already in English")
assert out == "This text is already in English"
assert translator.router.calls == [] # LLM not invoked

async def test_to_korean_skips_when_already_korean(self, translator):
text = "이미 한국어로 작성된 텍스트입니다"
out = await translator.translate_to_korean(text)
assert out == text
assert translator.router.calls == []

async def test_to_english_empty_returns_empty(self, translator):
assert await translator.translate_to_english(" ") == ""

async def test_to_korean_empty_returns_empty(self, translator):
assert await translator.translate_to_korean("") == ""


class TestWrapperStripping:
async def test_strips_prefix_and_trailing_separator_ko_to_en(self):
router = FakeRouter(reply="English translation: Hello world\n---")
t = ContentTranslator(router=router)
out = await t.translate_to_english("한국어 텍스트를 영어로 번역해 주세요 부탁합니다")
assert out == "Hello world"
assert len(router.calls) == 1

async def test_strips_prefix_en_to_ko(self):
router = FakeRouter(reply="Korean translation: 안녕하세요")
t = ContentTranslator(router=router)
out = await t.translate_to_korean("Please translate this English sentence to Korean")
assert out == "안녕하세요"

async def test_strips_lowercase_prefix_en_to_ko(self):
# Prefix removal is case-insensitive (consistent with KO→EN).
router = FakeRouter(reply="korean translation: 안녕하세요")
t = ContentTranslator(router=router)
out = await t.translate_to_korean("Please translate this English sentence to Korean")
assert out == "안녕하세요"

async def test_translation_failure_returns_empty(self):
class BoomRouter:
async def route(self, **kwargs):
raise RuntimeError("llm down")

t = ContentTranslator(router=BoomRouter())
out = await t.translate_to_english("한국어 텍스트 번역 요청 실패 케이스 테스트")
assert out == ""


class TestEnsureBilingual:
async def test_korean_input_keeps_korean_translates_english(self):
router = FakeRouter(reply="English version")
t = ContentTranslator(router=router)
korean = "한국어 원본 텍스트입니다 영어로 번역되어야 합니다"
en, ko = await t.ensure_bilingual(korean)
assert ko == korean
assert en == "English version"

async def test_english_input_keeps_english_translates_korean(self):
router = FakeRouter(reply="한국어 번역본")
t = ContentTranslator(router=router)
english = "English original content that should be translated to Korean"
en, ko = await t.ensure_bilingual(english)
assert en == english
assert ko == "한국어 번역본"

async def test_empty_returns_pair_of_empties(self, translator):
assert await translator.ensure_bilingual("") == ("", "")
Loading