"""v0.4.0 feature tests: chat history filter, file uploads, memory graph, diff. Run with: python3 tests/run_all.py (or: python3 tests/test_v040.py) """ import sys import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parent)) from harness import Harness # noqa: E402 import server # noqa: E402 class ChatHistoryTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.h = Harness() cls.h.start() @classmethod def tearDownClass(cls): cls.h.stop() def _seed_message(self, content, agent="opencode"): msg = { "id": "seed1", "role": "user", "agent": agent, "content": content, "timestamp": "2026-08-11T12:00:00Z", } history = server.load_chat_history() history.setdefault("messages", []).append(msg) server.save_chat_message(msg) def test_history_empty_by_default(self): # Clear any messages seeded by prior tests in this class server.save_chat_message({"id": "clear", "role": "user", "agent": "opencode", "content": "", "timestamp": ""}) history = server.load_chat_history() history["messages"] = [] server.CHAT_HISTORY_FILE.write_text(__import__("json").dumps(history)) status, data = self.h.api("GET", "/api/chat/history") self.assertEqual(status, 200) self.assertEqual(data["total"], 0) def test_history_search_filters(self): self._seed_message("terraform plan complete", "opencode") self._seed_message("pod restart completed", "hermes") status, data = self.h.api("GET", "/api/chat/history?q=terraform") self.assertEqual(status, 200) self.assertEqual(data["total"], 1) self.assertIn("terraform", data["messages"][0]["content"]) def test_history_agent_filter(self): self._seed_message("deploy the cluster", "opencode") self._seed_message("schedule the job", "hermes") status, data = self.h.api("GET", "/api/chat/history?agent=hermes") self.assertEqual(status, 200) self.assertEqual(data["total"], 1) self.assertEqual(data["messages"][0]["agent"], "hermes") def test_history_limit(self): for i in range(5): self._seed_message(f"message number {i}") status, data = self.h.api("GET", "/api/chat/history?limit=2") self.assertEqual(status, 200) self.assertEqual(data["total"], 2) class ChatUploadTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.h = Harness() cls.h.start() @classmethod def tearDownClass(cls): cls.h.stop() def test_upload_valid_file(self): with mock.patch.object(server, "execute_agent", return_value="mock reply"): status, data = self.h.upload( "/api/chat/upload", {"agent": "opencode", "message": "review this"}, "notes.md", b"# Notes\n\nsome content here", ) self.assertEqual(status, 200) self.assertEqual(data["status"], "ok") self.assertEqual(data["file"], "notes.md") self.assertEqual(data["response"]["content"], "mock reply") # File must be stored in the isolated temp uploads dir saved = self.h.tmp / "data" / "uploads" self.assertTrue(saved.exists()) files = list(saved.glob("*.md")) self.assertEqual(len(files), 1) self.assertIn(b"# Notes", files[0].read_bytes()) def test_upload_agent_validation(self): status, _ = self.h.upload( "/api/chat/upload", {"agent": "bogus", "message": ""}, "f.txt", b"x", ) self.assertEqual(status, 400) def test_upload_invalid_extension(self): status, _ = self.h.upload( "/api/chat/upload", {"agent": "opencode", "message": ""}, "virus.exe", b"x", ) self.assertEqual(status, 400) def test_upload_oversized_file(self): status, _ = self.h.upload( "/api/chat/upload", {"agent": "opencode", "message": ""}, "big.txt", b"x" * (2 * 1024 * 1024 + 1), ) self.assertEqual(status, 413) def test_upload_traversal_filename_rejected(self): status, _ = self.h.upload( "/api/chat/upload", {"agent": "opencode", "message": ""}, "..%2F..%2Fevil.txt", b"x", ) self.assertEqual(status, 400) class MemoryGraphTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.h = Harness() cls.h.start() @classmethod def tearDownClass(cls): cls.h.stop() def test_memory_graph_endpoint(self): status, data = self.h.api("GET", "/api/memory/graph") self.assertEqual(status, 200) self.assertIn("nodes", data) self.assertIn("edges", data) self.assertIsInstance(data["nodes"], list) self.assertIsInstance(data["edges"], list) def test_memory_graph_has_nodes(self): (self.h.tmp / "brain" / "graph-note.md").write_text( "zephyrquadrant graph connectivity notes" ) status, _ = self.h.api("POST", "/api/memory/reindex") self.assertEqual(status, 200) status, data = self.h.api("GET", "/api/memory/graph") self.assertEqual(status, 200) self.assertGreater(len(data["nodes"]), 0) class DiffViewerTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.h = Harness() cls.h.start() @classmethod def tearDownClass(cls): cls.h.stop() def test_diff_missing_file_param(self): status, _ = self.h.api("GET", "/api/diff") self.assertEqual(status, 400) def test_diff_nonexistent_file(self): status, _ = self.h.api("GET", "/api/diff?file=no/such/file.md") self.assertEqual(status, 404) def test_diff_traversal_rejected(self): status, _ = self.h.api("GET", "/api/diff?file=../../etc/passwd") self.assertEqual(status, 400) def test_diff_known_file_ok(self): # skills/ exists in the isolated state; git may not report a diff, # but the endpoint must still return 200 with changed=false status, data = self.h.api("GET", "/api/diff?file=skills/_template/SKILL.md") self.assertEqual(status, 200) self.assertIn("file", data) self.assertIn("changed", data) if __name__ == "__main__": unittest.main()