G0~G8 성과·동맹 측정 OS 작업 일괄 고정

8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본
폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을
git 이력으로 고정하는 것이 목적이다.

- contracts/routes/services: measurement, outcome_trajectory, rupture_repair,
  deliberate_practice, calibration_transfer, supervision_research,
  multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트
- infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행)
- apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가
- docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG
- scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트

engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
Yun Chan 2026-08-08 01:30:53 +09:00
parent 93dd8f82d7
commit 16e791e044
390 changed files with 243188 additions and 499 deletions

View file

@ -1,5 +1,6 @@
import asyncio
import json
import secrets
import shutil
import subprocess
import unittest
@ -8,6 +9,7 @@ from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from jsonschema import Draft202012Validator
from fastapi.testclient import TestClient
from app import engine_client
from app.contracts import engine_gateway as contract
@ -195,6 +197,106 @@ class _FakeStreamSession:
self.closed = True
class GatewayAuthenticationTest(unittest.TestCase):
SECRET = "engine-gateway-test-secret-" + ("x" * 32)
def test_unset_secret_preserves_local_gateway_compatibility(self):
with patch.object(gateway, "ENGINE_GATEWAY_SHARED_SECRET", ""):
response = TestClient(gateway.app).delete("/session/not-running")
self.assertEqual(response.status_code, 200)
def test_configured_secret_rejects_missing_and_wrong_credentials(self):
with patch.object(gateway, "ENGINE_GATEWAY_SHARED_SECRET", self.SECRET):
client = TestClient(gateway.app)
missing = client.post("/v1/generate", json={})
wrong = client.post(
"/v1/generate",
json={},
headers={gateway.ENGINE_TOKEN_HEADER: "wrong"},
)
authenticated = client.post(
"/v1/generate",
json={},
headers={gateway.ENGINE_TOKEN_HEADER: self.SECRET},
)
self.assertEqual(missing.status_code, 401)
self.assertEqual(wrong.status_code, 401)
self.assertEqual(authenticated.status_code, 422)
def test_health_probe_remains_unauthenticated(self):
with patch.object(gateway, "ENGINE_GATEWAY_SHARED_SECRET", self.SECRET):
response = TestClient(gateway.app).get("/health")
self.assertEqual(response.status_code, 200)
def test_ready_probe_requires_credentials_because_it_runs_generation(self):
with patch.object(gateway, "ENGINE_GATEWAY_SHARED_SECRET", self.SECRET):
response = TestClient(gateway.app).get("/ready")
self.assertEqual(response.status_code, 401)
def test_gateway_rejects_weak_configured_secret_at_startup(self):
for value in ("too-short", "example-gateway-secret-with-32-characters"):
with (
self.subTest(value=value),
patch.dict(
gateway.os.environ,
{"ENGINE_GATEWAY_SHARED_SECRET": value},
),
self.assertRaises(RuntimeError),
):
gateway._load_gateway_shared_secret()
def test_invalid_token_uses_constant_time_comparison(self):
with (
patch.object(gateway, "ENGINE_GATEWAY_SHARED_SECRET", self.SECRET),
patch.object(
gateway.secrets,
"compare_digest",
wraps=secrets.compare_digest,
) as compare_digest,
):
response = TestClient(gateway.app).get(
"/v1/capabilities",
headers={gateway.ENGINE_TOKEN_HEADER: "wrong"},
)
self.assertEqual(response.status_code, 401)
compare_digest.assert_called_once_with("wrong", self.SECRET)
def test_openapi_schema_does_not_expose_secret_or_auth_header(self):
with patch.object(gateway, "ENGINE_GATEWAY_SHARED_SECRET", self.SECRET):
schema = json.dumps(gateway.app.openapi())
self.assertNotIn(self.SECRET, schema)
self.assertNotIn(gateway.ENGINE_TOKEN_HEADER, schema)
def test_engine_client_adds_gateway_token_to_default_headers(self):
with patch.object(engine_client.httpx, "AsyncClient") as async_client_cls:
client = engine_client.EngineClient(
"http://127.0.0.1:9099",
shared_secret=self.SECRET,
)
client._new_client()
self.assertEqual(
async_client_cls.call_args.kwargs["headers"],
{gateway.ENGINE_TOKEN_HEADER: self.SECRET},
)
def test_engine_client_omits_gateway_token_when_unset(self):
with patch.object(engine_client.httpx, "AsyncClient") as async_client_cls:
client = engine_client.EngineClient(
"http://127.0.0.1:9099",
shared_secret="",
)
client._new_client()
self.assertEqual(async_client_cls.call_args.kwargs["headers"], {})
class GatewayModelTest(unittest.TestCase):
def test_contract_owns_gateway_default_model_sentinel(self):
self.assertEqual(contract.ENGINE_GATEWAY_DEFAULT_MODEL_SENTINEL, "gateway-default")
@ -456,6 +558,35 @@ class GatewayModelTest(unittest.TestCase):
{"reply": "embedded"},
)
def test_generate_response_structured_payload_repairs_only_trailing_commas(self):
response = contract.GenerateResponse(
text=(
'```json\n'
'{"reply":"keep literal , } and escaped \\\" text",'
'"items":[{"value":1,},],}\n'
'```'
),
provider="test",
model="test-model",
)
self.assertEqual(
contract.structured_payload_from_response(response),
{
"reply": 'keep literal , } and escaped " text',
"items": [{"value": 1}],
},
)
def test_generate_response_structured_payload_does_not_repair_other_corruption(self):
response = contract.GenerateResponse(
text='{"reply":"missing separator" "items":[]}',
provider="test",
model="test-model",
)
self.assertIsNone(contract.structured_payload_from_response(response))
def test_generate_response_structured_payload_rejects_non_object_json(self):
response = contract.GenerateResponse(
text='["not", "object"]',
@ -610,7 +741,25 @@ class GatewayModelTest(unittest.TestCase):
"type": "assistant",
"message": {"content": [{"type": "text", "text": "안녕!"}]},
},
{"type": "result", "is_error": False, "total_cost_usd": 0.01},
{
"type": "result",
"is_error": False,
"total_cost_usd": 0.01,
"modelUsage": {
"claude-opus-4-8": {
"inputTokens": 12,
"outputTokens": 7,
"cacheReadInputTokens": 101,
"cacheCreationInputTokens": 23,
},
"claude-haiku-4-5": {
"inputTokens": 3,
"outputTokens": 2,
"cacheReadInputTokens": 9,
"cacheCreationInputTokens": 0,
},
},
},
]
)
session = gateway.EngineSession()
@ -630,6 +779,8 @@ class GatewayModelTest(unittest.TestCase):
"type": "done",
"text": "안녕!",
"cost_usd": 0.01,
"tokens_in": 148,
"tokens_out": 9,
"turns": 1,
"is_error": False,
"error": "안녕!",
@ -637,6 +788,66 @@ class GatewayModelTest(unittest.TestCase):
],
)
def test_engine_session_uses_terminal_result_when_assistant_event_is_absent(self):
process = _FakeProcess()
process.stdin = _StreamStdin()
process.stdout = _StreamStdout(
[
{
"type": "result",
"is_error": False,
"result": '{"goal":{"score":0.2}}',
"usage": {"input_tokens": 10, "output_tokens": 5},
}
]
)
session = gateway.EngineSession()
session.proc = process
result = asyncio.run(session.turn("평가"))
self.assertEqual(result["text"], '{"goal":{"score":0.2}}')
self.assertFalse(result["is_error"])
def test_engine_session_streams_terminal_result_when_assistant_event_is_absent(self):
process = _FakeProcess()
process.stdin = _StreamStdin()
process.stdout = _StreamStdout(
[
{
"type": "result",
"is_error": False,
"result": "terminal-only",
"usage": {"input_tokens": 10, "output_tokens": 2},
}
]
)
session = gateway.EngineSession()
session.proc = process
async def collect():
return [event async for event in session.turn_stream("질문")]
events = asyncio.run(collect())
self.assertEqual(events[0], {"type": "delta", "text": "terminal-only"})
self.assertEqual(events[-1]["text"], "terminal-only")
def test_claude_result_tokens_falls_back_to_top_level_usage(self):
self.assertEqual(
gateway._claude_result_tokens(
{
"usage": {
"input_tokens": 17,
"output_tokens": 5,
"cache_read_input_tokens": 200,
"cache_creation_input_tokens": 30,
}
}
),
(247, 5),
)
def test_v1_generate_routes_non_claude_provider_through_registry(self):
result = SimpleNamespace(
text="registry response",
@ -770,7 +981,13 @@ class GatewayModelTest(unittest.TestCase):
async def fake_turn(content, timeout=120.0):
calls.append((content, timeout))
return {"text": "reused response", "cost_usd": 0.01, "is_error": False}
return {
"text": "reused response",
"cost_usd": 0.01,
"tokens_in": 321,
"tokens_out": 12,
"is_error": False,
}
async def fake_close():
closes.append(True)
@ -784,6 +1001,8 @@ class GatewayModelTest(unittest.TestCase):
self.assertEqual(validated.text, "reused response")
self.assertEqual(validated.provider, "claude_cli")
self.assertEqual(validated.cost_usd, 0.01)
self.assertEqual(validated.tokens_in, 321)
self.assertEqual(validated.tokens_out, 12)
self.assertEqual(
calls,
[("[이번 상담자 발화]\nhello", gateway.GENERATE_TURN_TIMEOUT_SECONDS)],
@ -801,7 +1020,13 @@ class GatewayModelTest(unittest.TestCase):
async def fake_turn(self, content, timeout=120.0):
turned.append((self, content, timeout))
return {"text": "fresh response", "cost_usd": 0.02, "is_error": False}
return {
"text": "fresh response",
"cost_usd": 0.02,
"tokens_in": 654,
"tokens_out": 21,
"is_error": False,
}
async def fake_close(self):
closed.append(self)
@ -817,6 +1042,8 @@ class GatewayModelTest(unittest.TestCase):
self.assertEqual(validated.text, "fresh response")
self.assertEqual(validated.provider, "claude_cli")
self.assertEqual(validated.cost_usd, 0.02)
self.assertEqual(validated.tokens_in, 654)
self.assertEqual(validated.tokens_out, 21)
self.assertEqual(len(started), 1)
self.assertEqual(
turned,
@ -850,7 +1077,13 @@ class GatewayModelTest(unittest.TestCase):
session = _FakeStreamSession(
[
{"type": "delta", "text": "안녕"},
{"type": "done", "cost_usd": 0.03, "turns": 2},
{
"type": "done",
"cost_usd": 0.03,
"tokens_in": 456,
"tokens_out": 18,
"turns": 2,
},
],
model="stream-model",
)
@ -868,6 +1101,8 @@ class GatewayModelTest(unittest.TestCase):
self.assertIn('"provider": "claude_cli"', body)
self.assertIn('"model": "stream-model"', body)
self.assertIn('"cost_usd": 0.03', body)
self.assertIn('"tokens_in": 456', body)
self.assertIn('"tokens_out": 18', body)
self.assertEqual(session.content, "[이번 상담자 발화]\nhello")
self.assertEqual(session.timeout, 600.0)
self.assertTrue(session.closed)