greydgl--pentestgpt
b9869307d0
* fix: 🐛 minor typo and build process * feat: 🎸 [WIP] Pentest mode * feat: 🎸 code abstraction * feat: modernize legacy PentestGPT with native multi-LLM support (#469) Rebuild the classic USENIX-2024 interactive PentestGPT (reasoning / generation / parsing sessions + Pentesting Task Tree + REPL) as a standalone `pentestgpt_legacy` package on a native per-provider LLM layer that supports the latest 2026 models. - llm/: BaseProvider + OpenAI-compatible / Anthropic / Gemini connectors, a web-verified model registry (OpenAI, Anthropic, Gemini, DeepSeek, xAI, Qwen, Moonshot, local Ollama), a factory, and an LLMClient bridging async providers to the core's synchronous send_new_message/send_message session API. - CLI `pentestgpt-legacy`: --list-models and --smoke-test (live per-model round-trip matrix), plus --reasoning-model / --parsing-model / --base-url. - Tests: 25 unit tests (mocked, no network). Live smoke test verified 22/22 models with a configured key respond. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(backend): address review on ClaudeCodeBackend subprocess handling - _build_env: pop ANTHROPIC_API_KEY instead of setting it to "", so an empty value can't shadow the CLI's own auth fallback (e.g. subscription login). - _kill_process: reap the force-killed process with os.waitpid(.., WNOHANG) instead of calling the proc.wait() coroutine without awaiting it (removes the "coroutine was never awaited" warning). - query/_drain_stderr: drain subprocess stderr in a background task so its pipe buffer can't fill and deadlock the child. Also reformats backend.py, fixing the failing Lint (ruff format) check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(docker-test): assert uv instead of Poetry in container health check The project migrated from Poetry to uv (the Dockerfile installs uv to /home/pentester/.local/bin, which is on PATH), so test_poetry_installed failed with exit 127. Replace it with test_uv_installed checking `uv --version`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
79 行
2.6 KiB
Python
79 行
2.6 KiB
Python
"""Tests for the synchronous LLMClient session bridge."""
|
|
|
|
import pytest
|
|
|
|
from pentestgpt_legacy.llm.base import BaseProvider, Message
|
|
from pentestgpt_legacy.llm.client import LLMClient
|
|
from pentestgpt_legacy.llm.registry import ModelSpec, ProviderInfo
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
class FakeProvider(BaseProvider):
|
|
"""Records calls and echoes a deterministic reply."""
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__(ProviderInfo(key="fake", label="Fake", kind="openai"), None, None)
|
|
self.calls: list[tuple[list[Message], str | None]] = []
|
|
|
|
async def acomplete(
|
|
self,
|
|
messages: list[Message],
|
|
system: str | None,
|
|
spec: ModelSpec,
|
|
*,
|
|
max_output_tokens: int | None = None,
|
|
temperature: float | None = None,
|
|
) -> str:
|
|
self.calls.append(([dict(m) for m in messages], system))
|
|
return f"reply-{len(messages)}"
|
|
|
|
|
|
def _spec(context_window: int = 100_000) -> ModelSpec:
|
|
return ModelSpec(
|
|
id="fake-model", provider="fake", context_window=context_window, tier="balanced"
|
|
)
|
|
|
|
|
|
def test_send_new_message_creates_conversation() -> None:
|
|
provider = FakeProvider()
|
|
client = LLMClient(provider, _spec())
|
|
text, cid = client.send_new_message("hello")
|
|
assert text == "reply-1"
|
|
assert cid in client.conversations
|
|
history = client.conversations[cid]
|
|
assert history[0] == {"role": "user", "content": "hello"}
|
|
assert history[1] == {"role": "assistant", "content": "reply-1"}
|
|
|
|
|
|
def test_send_message_continues_conversation() -> None:
|
|
provider = FakeProvider()
|
|
client = LLMClient(provider, _spec())
|
|
_, cid = client.send_new_message("first")
|
|
client.send_message("second", cid)
|
|
assert len(client.conversations[cid]) == 4
|
|
# second call saw both prior turns + the new user message
|
|
last_messages, _system = provider.calls[-1]
|
|
assert last_messages[0]["role"] == "user"
|
|
assert last_messages[-1] == {"role": "user", "content": "second"}
|
|
|
|
|
|
def test_system_prompt_forwarded() -> None:
|
|
provider = FakeProvider()
|
|
client = LLMClient(provider, _spec(), system_prompt="SYS")
|
|
client.send_new_message("hi")
|
|
_messages, system = provider.calls[-1]
|
|
assert system == "SYS"
|
|
|
|
|
|
def test_history_trimmed_to_length_and_starts_with_user() -> None:
|
|
provider = FakeProvider()
|
|
client = LLMClient(provider, _spec(), history_length=2)
|
|
_, cid = client.send_new_message("turn1")
|
|
for i in range(5):
|
|
client.send_message(f"turn-{i}", cid)
|
|
# the provider only ever received at most history_length messages
|
|
for messages, _system in provider.calls:
|
|
assert len(messages) <= 2
|
|
assert messages[0]["role"] == "user"
|