Make the first RAG slice executable without risking production data
Some checks failed
verify / verify (push) Has been cancelled

The Stage 1 foundation now proves provider contracts with mocks and validates PostgreSQL/pgvector ingestion, approval binding, retrieval, reranking, and idempotency using only synthetic data. Live Bailian validation remains gated on rotating the exposed key.

Constraint: The key shown in chat is compromised and cannot be used or committed

Rejected: Mark Stage 1 complete from mock and offline results | real three-model smoke is still required

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Do not enable real-data ingestion until Stage 3 cloud approval and outbound manifest controls are enforced end to end

Tested: make verify; 41 pytest tests; strict mypy; Ruff; Compose config; pinned image build; empty-volume migration; role denial; two idempotent 20-vector seeds; database restart persistence

Not-tested: Live Bailian calls require a newly rotated key; React product UI is not implemented
This commit is contained in:
2026-07-12 15:41:58 +08:00
parent ec1acb36b5
commit f4ba5d5342
61 changed files with 6886 additions and 20 deletions

View File

@@ -1 +0,0 @@

View File

@@ -0,0 +1,74 @@
from pathlib import Path
import pytest
from app.core.config import Settings
from app.core.secrets import SecretFileError, read_secret_file
def test_secret_file_strips_single_trailing_newline(tmp_path: Path) -> None:
secret_path = tmp_path / "secret"
secret_path.write_text("test-only-value\n", encoding="utf-8")
assert read_secret_file(secret_path) == "test-only-value"
def test_secret_error_never_contains_secret_value(tmp_path: Path) -> None:
secret_path = tmp_path / "secret"
secret_path.write_text("first\nsecond\n", encoding="utf-8")
with pytest.raises(SecretFileError) as captured:
read_secret_file(secret_path)
assert "first" not in str(captured.value)
assert "second" not in str(captured.value)
def test_database_url_reads_password_from_file(tmp_path: Path) -> None:
password_path = tmp_path / "postgres_password"
password_path.write_text("local-test-password", encoding="utf-8")
settings = Settings(postgres_password_file=password_path)
url = settings.database_url()
assert url.password == "local-test-password"
assert "local-test-password" not in str(url)
def test_base_urls_drop_trailing_slashes() -> None:
settings = Settings(
bailian_openai_base_url="https://example.invalid/compatible-mode/v1/",
bailian_native_base_url="https://example.invalid/api/v1/",
bailian_rerank_base_url="https://example.invalid/compatible-api/v1/",
)
assert settings.bailian_openai_base_url.endswith("/v1")
assert settings.bailian_rerank_base_url.endswith("/v1")
def test_live_endpoints_must_share_one_beijing_workspace(tmp_path: Path) -> None:
key_path = tmp_path / "key"
key_path.write_text("test-key-value", encoding="utf-8")
settings = Settings(
dashscope_api_key_file=key_path,
bailian_openai_base_url=(
"https://workspace-a.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"
),
bailian_rerank_base_url=(
"https://workspace-b.cn-beijing.maas.aliyuncs.com/compatible-api/v1"
),
)
with pytest.raises(ValueError, match="same workspace"):
settings.bailian_api_key()
def test_live_endpoint_rejects_non_bailian_host_before_reading_key(tmp_path: Path) -> None:
settings = Settings(
dashscope_api_key_file=tmp_path / "missing",
bailian_openai_base_url="https://attacker.invalid/compatible-mode/v1",
bailian_rerank_base_url="https://attacker.invalid/compatible-api/v1",
)
with pytest.raises(ValueError, match="approved Beijing"):
settings.bailian_api_key()

View File

@@ -0,0 +1,37 @@
import json
from pathlib import Path
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[3]
SAMPLE_ROOT = PROJECT_ROOT / "data" / "samples" / "public"
def load_jsonl(path: Path) -> list[dict[str, Any]]:
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
def test_demo_corpus_is_synthetic_pending_hash_bound_approval_and_unique() -> None:
documents = load_jsonl(SAMPLE_ROOT / "demo_documents.jsonl")
assert len(documents) == 20
assert len({document["doc_id"] for document in documents}) == 20
assert all(document["source_type"] == "synthetic" for document in documents)
assert all(
document["review_state"] == "LOCAL_PARSED_PENDING_CLOUD_REVIEW" for document in documents
)
assert all(document["cloud_policy_id"] == "synthetic-demo-v1" for document in documents)
assert all("cloud_approved" not in document for document in documents)
assert all(
"虚构" in document["content"] or "演示" in document["content"] for document in documents
)
def test_demo_queries_only_reference_existing_documents() -> None:
documents = load_jsonl(SAMPLE_ROOT / "demo_documents.jsonl")
queries = load_jsonl(SAMPLE_ROOT / "demo_queries.jsonl")
document_ids = {document["doc_id"] for document in documents}
assert len(queries) == 10
assert len({query["qid"] for query in queries}) == 10
assert all(set(query["expected_doc_ids"]) <= document_ids for query in queries)
assert any(not query["answerable"] and not query["expected_doc_ids"] for query in queries)

View File

@@ -0,0 +1,47 @@
import math
import pytest
from app.adapters.fake import FakeEmbeddingProvider, FakeReranker
from app.ports.model_providers import ModelProviderError
@pytest.mark.asyncio
async def test_fake_embedding_is_deterministic_normalized_and_1024_dimensional() -> None:
provider = FakeEmbeddingProvider()
first = await provider.embed_documents(["斑岩铜矿 钾化 绢英岩化"])
second = await provider.embed_query("斑岩铜矿 钾化 绢英岩化")
assert first.vectors[0] == second.vectors[0]
assert len(first.vectors[0]) == 1024
assert math.isclose(sum(value * value for value in first.vectors[0]), 1.0)
@pytest.mark.asyncio
async def test_fake_reranker_preserves_original_candidate_index() -> None:
reranker = FakeReranker()
documents = ["煤层测井对比", "斑岩铜矿钾化带", "铝土矿含矿层"]
result = await reranker.rerank("斑岩铜矿的钾化带", documents, top_n=2)
assert result.items[0].index == 1
assert result.items[0].document == documents[1]
assert result.items[0].relevance_score >= result.items[1].relevance_score
@pytest.mark.asyncio
async def test_fake_providers_reject_inputs_the_live_contract_rejects() -> None:
embedder = FakeEmbeddingProvider()
reranker = FakeReranker()
with pytest.raises(ModelProviderError):
await embedder.embed_documents([])
with pytest.raises(ModelProviderError):
await embedder.embed_documents(["x"] * 11)
with pytest.raises(ModelProviderError):
await reranker.rerank("", ["document"], top_n=1)
with pytest.raises(ModelProviderError):
await reranker.rerank("query", [], top_n=1)
with pytest.raises(ModelProviderError):
await reranker.rerank("query", ["document"], top_n=0)

View File

@@ -0,0 +1,30 @@
import httpx
import pytest
from app.main import app
@pytest.mark.asyncio
async def test_liveness_does_not_require_database_or_model_credentials() -> None:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/health/live")
assert response.status_code == 200
assert response.json()["status"] == "ok"
@pytest.mark.asyncio
async def test_meta_exposes_model_names_but_no_credentials() -> None:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/meta")
assert response.status_code == 200
payload = response.json()
assert payload["models"] == {
"embedding": "text-embedding-v4",
"rerank": "qwen3-rerank",
"generation": "deepseek-v4-flash",
}
assert "key" not in str(payload).lower()

View File

@@ -0,0 +1,50 @@
import json
from pathlib import Path
from typing import Any
import pytest
from app.adapters.fake import FakeEmbeddingProvider, FakeReranker
PROJECT_ROOT = Path(__file__).resolve().parents[3]
SAMPLE_ROOT = PROJECT_ROOT / "data" / "samples" / "public"
def load_jsonl(path: Path) -> list[dict[str, Any]]:
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
def cosine(left: tuple[float, ...], right: tuple[float, ...]) -> float:
return sum(
left_value * right_value for left_value, right_value in zip(left, right, strict=True)
)
@pytest.mark.asyncio
async def test_synthetic_questions_retrieve_expected_document_after_rerank() -> None:
documents = load_jsonl(SAMPLE_ROOT / "demo_documents.jsonl")
queries = load_jsonl(SAMPLE_ROOT / "demo_queries.jsonl")
embedder = FakeEmbeddingProvider()
reranker = FakeReranker()
document_texts = [f"{item['title']}\n{item['content']}" for item in documents]
document_vectors: list[tuple[float, ...]] = []
for offset in range(0, len(document_texts), 10):
batch = await embedder.embed_documents(document_texts[offset : offset + 10])
document_vectors.extend(batch.vectors)
hits = 0
answerable_queries = [query for query in queries if query["answerable"]]
for query in answerable_queries:
query_vector = (await embedder.embed_query(query["query"])).vectors[0]
candidate_indexes = sorted(
range(len(documents)),
key=lambda index: cosine(query_vector, document_vectors[index]),
reverse=True,
)[:5]
candidate_texts = [document_texts[index] for index in candidate_indexes]
reranked = await reranker.rerank(query["query"], candidate_texts, top_n=3)
result_ids = [documents[candidate_indexes[item.index]]["doc_id"] for item in reranked.items]
if set(query["expected_doc_ids"]) & set(result_ids):
hits += 1
assert hits / len(answerable_queries) >= 0.8

View File

@@ -0,0 +1,26 @@
from app.ports.model_providers import ModelProviderError, ProviderErrorKind
from app.tools.provider_smoke import failed_probe
def test_failed_probe_only_exposes_sanitized_provider_fields() -> None:
error = ModelProviderError(
operation="embedding.create",
kind=ProviderErrorKind.AUTHENTICATION,
status_code=401,
provider_code="invalid_api_key",
request_id="req-safe-1",
)
result = failed_probe("embedding", error)
assert result.error_kind == "authentication"
assert result.status_code == 401
assert result.request_id == "req-safe-1"
assert "invalid_api_key" not in repr(result)
def test_unexpected_error_is_reduced_to_fixed_category() -> None:
result = failed_probe("chat", RuntimeError("sensitive content"))
assert result.error_kind == "internal_contract_error"
assert "sensitive content" not in repr(result)

View File

@@ -0,0 +1,38 @@
import subprocess
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[3]
SCANNER = PROJECT_ROOT / "scripts" / "check-secrets.sh"
def run_git(repository: Path, *arguments: str) -> None:
subprocess.run(
["git", *arguments],
cwd=repository,
check=True,
capture_output=True,
text=True,
)
def test_staged_scan_reads_index_blob_not_modified_worktree(tmp_path: Path) -> None:
run_git(tmp_path, "init", "--quiet")
run_git(tmp_path, "config", "user.email", "test@example.invalid")
run_git(tmp_path, "config", "user.name", "Secret Scanner Test")
candidate = tmp_path / "config.txt"
candidate.write_text("sk-" + "A" * 30, encoding="utf-8")
run_git(tmp_path, "add", "config.txt")
candidate.write_text("safe working-tree replacement", encoding="utf-8")
result = subprocess.run(
[str(SCANNER), "--staged"],
cwd=tmp_path,
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "config.txt" in result.stderr
assert "safe working-tree replacement" not in result.stderr
assert "A" * 30 not in result.stderr

View File

@@ -0,0 +1,54 @@
from pathlib import Path
import pytest
from app.adapters.fake import FakeEmbeddingProvider
from app.core.config import Settings
from app.tools.seed_demo import (
embed_in_batches,
embedding_profile_hash,
load_documents,
prepare_chunks,
)
PROJECT_ROOT = Path(__file__).resolve().parents[3]
DOCUMENTS_PATH = PROJECT_ROOT / "data" / "samples" / "public" / "demo_documents.jsonl"
@pytest.mark.asyncio
async def test_seed_preparation_batches_and_hash_binds_twenty_documents() -> None:
documents = load_documents(DOCUMENTS_PATH)
settings = Settings()
provider = FakeEmbeddingProvider()
texts = [
f"标题:{item.title}\n地区:{item.region}\n矿种:{item.mineral}\n正文:{item.content}"
for item in documents
]
vectors, model = await embed_in_batches(provider, texts)
chunks = prepare_chunks(
documents,
vectors,
profile_hash=embedding_profile_hash(settings, "fake"),
embedding_model=model,
)
assert len(chunks) == 20
assert all(len(item.vector) == 1024 for item in chunks)
assert all(item.embedding_text == item.embedding_prefix + item.cloud_text for item in chunks)
assert len({item.chunk_id for item in chunks}) == 20
assert all(len(item.outbound_manifest_sha256) == 64 for item in chunks)
def test_seed_rejects_fixture_not_explicitly_marked_synthetic(tmp_path: Path) -> None:
path = tmp_path / "documents.jsonl"
path.write_text(
'{"doc_id":"x","title":"x","content":"x","region":"x",'
'"mineral":"x","page_no":1,"source_type":"unknown",'
'"review_state":"LOCAL_PARSED_PENDING_CLOUD_REVIEW",'
'"cloud_policy_id":"synthetic-demo-v1"}\n',
encoding="utf-8",
)
with pytest.raises(ValueError, match="non_synthetic_document"):
load_documents(path)