feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestServer
|
||||
|
||||
from decision_server.credentials import deepseek_llm, openrouter_jev
|
||||
from decision_server.protocol import DecisionError
|
||||
from decision_server.providers.codex import CodexAccount
|
||||
from decision_server.providers.codex_gate import verify_no_tools
|
||||
from decision_server.providers.jev import answer
|
||||
from decision_server.tests.test_service import plan_value, request_value
|
||||
|
||||
|
||||
class CredentialTests(unittest.TestCase):
|
||||
def test_explicit_single_variable_no_eval(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "env"
|
||||
path.write_text(
|
||||
"OTHER_SECRET=do-not-import\n"
|
||||
'export OPENROUTER_API_KEY="fixture-key-at-least-16" # note\n'
|
||||
)
|
||||
conn = openrouter_jev(path)
|
||||
self.assertEqual(conn.protocol, "openrouter-decisions")
|
||||
self.assertEqual(conn.model, "typesafe/jev-1.13")
|
||||
self.assertNotIn("OTHER_SECRET", os.environ)
|
||||
path.write_text("DEEPSEEK_API_KEY=another-fixture-key-16\n")
|
||||
self.assertEqual(deepseek_llm(path).model, "deepseek-flash")
|
||||
self.assertEqual(deepseek_llm(path).base_url, "https://api.deepseek.com")
|
||||
for value in ("$(touch PWNED)", "`some-command`", "short"):
|
||||
path.write_text("OPENROUTER_API_KEY=" + value)
|
||||
with self.assertRaises(DecisionError):
|
||||
openrouter_jev(path)
|
||||
path.write_text(
|
||||
"OPENROUTER_API_KEY=fixture-key-at-least-16\nOPENROUTER_API_KEY=duplicate-fixture-16"
|
||||
)
|
||||
with self.assertRaises(DecisionError):
|
||||
openrouter_jev(path)
|
||||
|
||||
def test_official_choice_not_reordered(self):
|
||||
result = {"answers": {"test": {"choice": "a", "probabilities": {"a": 0.1, "b": 0.9}}}}
|
||||
self.assertEqual(answer(result, "test", ["a", "b"]), "a")
|
||||
for value in (float("nan"), True, -1, 1.01):
|
||||
result["answers"]["test"]["probabilities"]["a"] = value
|
||||
with self.assertRaises(DecisionError):
|
||||
answer(result, "test", ["a", "b"])
|
||||
|
||||
|
||||
class CodexTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.client = CodexAccount(Path(self.temp.name))
|
||||
self.client.status = AsyncMock(return_value={"loggedIn": True})
|
||||
self.client.check_model = AsyncMock(return_value={"toolGatePassed": True})
|
||||
self.client.close = AsyncMock()
|
||||
self.calls = []
|
||||
self.mode = "success"
|
||||
self.started = asyncio.Event()
|
||||
|
||||
async def rpc(method, params):
|
||||
self.calls.append((method, params))
|
||||
if method == "thread/start":
|
||||
return {"thread": {"id": "thread"}}
|
||||
if method == "turn/start":
|
||||
queue = self.client.queues["thread"]
|
||||
self.started.set()
|
||||
if self.mode != "wait":
|
||||
item = {"type": "agentMessage", "text": json.dumps(plan_value())}
|
||||
if self.mode == "tool":
|
||||
item = {"type": "commandExecution"}
|
||||
queue.put_nowait(
|
||||
{
|
||||
"method": "item/completed",
|
||||
"params": {
|
||||
"threadId": "thread",
|
||||
"turnId": "turn",
|
||||
"item": item,
|
||||
},
|
||||
}
|
||||
)
|
||||
queue.put_nowait(
|
||||
{
|
||||
"method": "turn/completed",
|
||||
"params": {
|
||||
"threadId": "thread",
|
||||
"turn": {
|
||||
"id": "turn",
|
||||
"status": self.mode if self.mode == "failed" else "completed",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
return {"turn": {"id": "turn"}}
|
||||
return {}
|
||||
|
||||
self.client.rpc = AsyncMock(side_effect=rpc)
|
||||
self.client.process = SimpleNamespace(returncode=None)
|
||||
|
||||
async def asyncTearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
async def test_plan_is_structured_and_ephemeral(self):
|
||||
value, _ = await self.client.plan(request_value(), "allowed")
|
||||
self.assertEqual(value, plan_value())
|
||||
self.assertTrue(self.calls[0][1]["ephemeral"])
|
||||
self.assertIn("outputSchema", self.calls[1][1])
|
||||
self.assertEqual(self.calls[-1][0], "thread/unsubscribe")
|
||||
self.assertFalse(self.client.queues)
|
||||
|
||||
async def test_tool_error_and_failed_turn_interrupt(self):
|
||||
for mode in ("tool", "failed"):
|
||||
self.mode = mode
|
||||
self.calls.clear()
|
||||
with self.assertRaises(DecisionError):
|
||||
await self.client.plan(request_value(), "allowed")
|
||||
self.assertIn("turn/interrupt", [method for method, _ in self.calls])
|
||||
|
||||
async def test_cancel_uses_exact_turn_interrupt(self):
|
||||
self.mode = "wait"
|
||||
task = asyncio.create_task(self.client.plan(request_value(), "allowed"))
|
||||
await self.started.wait()
|
||||
task.cancel()
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
await task
|
||||
self.assertIn(("turn/interrupt", {"threadId": "thread", "turnId": "turn"}), self.calls)
|
||||
self.assertFalse(self.client.queues)
|
||||
|
||||
async def test_failed_gate_never_starts_turn(self):
|
||||
self.client.check_model.side_effect = DecisionError("codex_tool_gate_failed")
|
||||
with self.assertRaises(DecisionError):
|
||||
await self.client.plan(request_value(), "allowed")
|
||||
self.assertFalse(self.calls)
|
||||
|
||||
async def test_no_login_no_turn(self):
|
||||
self.client.status.return_value = {"loggedIn": False}
|
||||
with self.assertRaises(DecisionError):
|
||||
await self.client.plan(request_value(), "allowed")
|
||||
self.assertFalse(self.calls)
|
||||
|
||||
async def test_login_cancel_and_logout_are_named_operations(self):
|
||||
self.client.start = AsyncMock()
|
||||
self.client.rpc = AsyncMock(
|
||||
return_value={
|
||||
"authUrl": "https://auth.openai.com/oauth/authorize?state=fixture",
|
||||
"loginId": "login",
|
||||
}
|
||||
)
|
||||
result = await self.client.login()
|
||||
self.assertEqual(result["storage"], "session-only")
|
||||
self.client.rpc.assert_awaited_with("account/login/start", {"type": "chatgpt"})
|
||||
await self.client.cancel_login()
|
||||
self.client.rpc.assert_awaited_with("account/login/cancel", {"loginId": "login"})
|
||||
self.assertIsNone(self.client.login_id)
|
||||
await self.client.logout()
|
||||
self.client.rpc.assert_awaited_with("account/logout", {})
|
||||
self.client.close.assert_awaited()
|
||||
|
||||
async def test_device_login_uses_official_protocol(self):
|
||||
self.client.start = AsyncMock()
|
||||
self.client.rpc = AsyncMock(
|
||||
return_value={
|
||||
"verificationUrl": "https://auth.openai.com/codex/device",
|
||||
"userCode": "ABCD-1234",
|
||||
"loginId": "device-login",
|
||||
}
|
||||
)
|
||||
result = await self.client.login(device=True)
|
||||
self.client.rpc.assert_awaited_with("account/login/start", {"type": "chatgptDeviceCode"})
|
||||
self.assertEqual(result["userCode"], "ABCD-1234")
|
||||
self.assertNotIn("authUrl", result)
|
||||
self.assertEqual(self.client.login_id, "device-login")
|
||||
|
||||
async def test_unexpected_auth_url_rejected(self):
|
||||
self.client.start = AsyncMock()
|
||||
self.client.rpc = AsyncMock(return_value={"authUrl": "https://evil.test/login"})
|
||||
with self.assertRaisesRegex(DecisionError, "codex_unexpected_login_url"):
|
||||
await self.client.login()
|
||||
self.client.close.assert_awaited()
|
||||
|
||||
async def test_hidden_or_unknown_model_rejected_before_gate(self):
|
||||
native = CodexAccount(Path(self.temp.name))
|
||||
native.models = AsyncMock(return_value={"models": [{"id": "current"}]})
|
||||
with self.assertRaisesRegex(DecisionError, "codex_model_unavailable"):
|
||||
await native.check_model("gpt-5.4")
|
||||
self.assertFalse(native.checked)
|
||||
|
||||
async def test_unknown_rpc_forbidden(self):
|
||||
native = CodexAccount(Path(self.temp.name))
|
||||
with self.assertRaises(DecisionError):
|
||||
await native.rpc("command/exec", {})
|
||||
with (
|
||||
patch("decision_server.providers.codex.shutil.which", return_value=None),
|
||||
self.assertRaisesRegex(DecisionError, "codex_not_installed"),
|
||||
):
|
||||
await native.start()
|
||||
|
||||
|
||||
class NativeGates(unittest.IsolatedAsyncioTestCase):
|
||||
@unittest.skipUnless(
|
||||
os.environ.get("DECISION_CODEX_SMOKE") == "1", "native fake inference is opt-in"
|
||||
)
|
||||
async def test_native_structured_turn_and_unsubscribe_without_login(self):
|
||||
async def respond(request):
|
||||
incoming = await request.json()
|
||||
self.assertEqual(incoming.get("tools", []), [])
|
||||
item = {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"id": "msg_plan",
|
||||
"status": "completed",
|
||||
"phase": "final_answer",
|
||||
"content": [{"type": "output_text", "text": json.dumps(plan_value())}],
|
||||
}
|
||||
events = [
|
||||
{
|
||||
"type": "response.created",
|
||||
"response": {"id": "resp_plan", "status": "in_progress"},
|
||||
},
|
||||
{"type": "response.output_item.done", "output_index": 0, "item": item},
|
||||
{
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp_plan",
|
||||
"status": "completed",
|
||||
"output": [item],
|
||||
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
|
||||
},
|
||||
},
|
||||
]
|
||||
return web.Response(
|
||||
content_type="text/event-stream",
|
||||
text="".join(
|
||||
"event: " + e["type"] + "\ndata: " + json.dumps(e) + "\n\n" for e in events
|
||||
),
|
||||
)
|
||||
|
||||
app = web.Application()
|
||||
app.router.add_post("/v1/responses", respond)
|
||||
server = TestServer(app)
|
||||
await server.start_server()
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
client = CodexAccount(Path(directory), probe_url=str(server.make_url("/v1")))
|
||||
try:
|
||||
await client.start()
|
||||
# Fake-inference fixture only: no OAuth or cloud inference.
|
||||
with (
|
||||
patch.object(client, "status", AsyncMock(return_value={"loggedIn": True})),
|
||||
patch.object(client, "check_model", AsyncMock()),
|
||||
):
|
||||
result, _ = await client.plan(request_value(), "gpt-5.6-terra")
|
||||
self.assertEqual(result, plan_value())
|
||||
self.assertIsNotNone(client.process)
|
||||
self.assertIsNone(client.process.returncode)
|
||||
finally:
|
||||
await client.close()
|
||||
finally:
|
||||
await server.close()
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.environ.get("DECISION_CODEX_SMOKE") == "1", "native offline gate is opt-in"
|
||||
)
|
||||
async def test_all_visible_models_no_tools_and_injected_calls_rejected(self):
|
||||
catalog = Path(__file__).parents[1] / "providers/codex_models_0_147.json"
|
||||
models = json.loads(catalog.read_text())["models"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for model in models:
|
||||
if model["visibility"] != "list":
|
||||
continue
|
||||
with self.subTest(model=model["slug"]):
|
||||
evidence = {}
|
||||
await verify_no_tools(Path(directory), model["slug"], evidence)
|
||||
self.assertTrue(evidence["passed"])
|
||||
self.assertEqual(evidence["tools"], [])
|
||||
Reference in New Issue
Block a user