"""Intensive bug-hunting tests for StoryCode. Covers edge cases, error paths, boundary conditions, and integration across the entire codebase. Run: python tests/test_bugs.py """ from __future__ import annotations import json import os import sys import tempfile import zipfile sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import config from analyzer import analyze_project from analyzer import deps as deps_mod from analyzer import graph, generic, python_ast, js_treesitter from db import init_db, save_story, list_stories, get_story from diagram import build_mermaid, render_html from ingest import from_folder, from_text, from_zip, redact_secrets, SourceFile, Ingested from llm import _loads, _without_thinking, LLMUnavailable from schema import ( FileInfo, FileSummary, ProjectModel, ProjectStory, StorySection, file_summary_schema, project_story_schema, Symbol, Dependency, ) from story import map_prompt, reduce_prompt, plain_fallback_story, file_digest, project_digest import narrate import story as story_mod PASSED = 0 FAILED = 0 BUGS = [] def ok(name): global PASSED PASSED += 1 print(f" ok {name}") def bug(name, detail): global FAILED FAILED += 1 BUGS.append((name, detail)) print(f" BUG {name}: {detail}") def assert_eq(name, got, want): if got == want: ok(name) else: bug(name, f"expected {want!r}, got {got!r}") def assert_true(name, cond, detail=""): if cond: ok(name) else: bug(name, detail or "condition was false") def assert_no_crash(name, fn, *args, **kwargs): try: fn(*args, **kwargs) ok(name) except Exception as exc: bug(name, f"crashed: {type(exc).__name__}: {exc}") # ============================================================ # 1. SECRET SCANNING # ============================================================ print("\n=== 1. SECRET SCANNING ===") def test_secret_scanning(): # All known patterns cases = [ ("OpenAI key", 'sk-abc123def456ghi789jkl012', "OpenAI"), ("Anthropic key", 'sk-ant-api03-abc123def456ghi789jkl', "Anthropic"), ("AWS key", 'AKIAIOSFODNN7EXAMPLE', "AWS"), ("Google API key", 'AIzaSyA1234567890abcdefghijklmnopqrstuv', "Google"), ("GitHub token", 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef', "GitHub"), ("Slack token", 'xoxb-123456789012-1234567890123-AbCdEfGh', "Slack"), ("Private key", '-----BEGIN RSA PRIVATE KEY-----', "Private"), ("Hard-coded password", 'password = "supersecretvalue123"', "Hard-coded"), ("Hard-coded api_key", 'api_key="abcdef1234567890"', "Hard-coded"), ] for label, payload, expected_hit in cases: clean, hits = redact_secrets(f'x = "{payload}"') if any(expected_hit in h for h in hits): ok(f"secret_{label}") else: bug(f"secret_{label}", f"expected hit containing '{expected_hit}', got {hits}") if "REDACTED" in clean: ok(f"redact_{label}") else: bug(f"redact_{label}", f"no REDACTION in: {clean}") # False positive: short strings should NOT be redacted clean, hits = redact_secrets('password = "abc"') if not hits: ok("secret_short_no_false_positive") else: bug("secret_short_no_false_positive", f"false positive on short string: {hits}") # Empty input clean, hits = redact_secrets("") assert_eq("secret_empty_text", clean, "") assert_eq("secret_empty_hits", hits, []) # No secrets clean, hits = redact_secrets("x = 42\nprint('hello')") assert_eq("secret_clean_code", hits, []) # Multiple secrets in one file text = 'key1 = "sk-abc123def456ghi789jkl012"\nkey2 = "AKIAIOSFODNN7EXAMPLE"' clean, hits = redact_secrets(text) assert_true("secret_multi_hits", len(hits) >= 2, f"got {len(hits)} hits") assert_true("secret_multi_redacted", "REDACTED" in clean) test_secret_scanning() # ============================================================ # 2. INGEST EDGE CASES # ============================================================ print("\n=== 2. INGEST EDGE CASES ===") def test_ingest(): # Empty folder with tempfile.TemporaryDirectory() as td: ingested = from_folder(td, name="empty") assert_eq("ingest_empty_files", len(ingested.files), 0) assert_eq("ingest_empty_name", ingested.name, "empty") # Single pasted file ingested = from_text("def hello(): pass", filename="hello.py") assert_eq("ingest_text_files", len(ingested.files), 1) assert_eq("ingest_text_lang", ingested.files[0].lang, "python") assert_eq("ingest_text_name", ingested.name, "hello.py") # Pasted file with secret ingested = from_text('API_KEY = "sk-abc123def456ghi789jkl012"', filename="config.py") assert_true("ingest_text_secret", len(ingested.secrets_found) > 0, f"expected secret hits, got {ingested.secrets_found}") # Zip with single folder wrapping with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "project.zip") with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("myapp/main.py", "x = 1") zf.writestr("myapp/utils.py", "y = 2") ingested = from_zip(zip_path, name="test") assert_true("ingest_zip_folder", len(ingested.files) >= 2, f"got {len(ingested.files)} files") # Zip with MACOSX junk with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "junk.zip") with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("__MACOSX/._main.py", "junk") zf.writestr("main.py", "x = 1") ingested = from_zip(zip_path) assert_true("ingest_macosx_skip", all("__MACOSX" not in f.path for f in ingested.files), f"MACOSX file leaked through") # Zip with binary-like files with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "binary.zip") with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("image.png", b"\x89PNG\r\n\x1a\n" + b"\x00" * 100) zf.writestr("code.py", "x = 1") ingested = from_zip(zip_path) assert_true("ingest_skip_binary", len(ingested.files) == 1, f"binary file not skipped, got {len(ingested.files)} files") # Binary file detection assert_true("ingest_binary_ext", config._lang_for if hasattr(config, '_lang_for') else True, "") # Size limits old_max = config.MAX_FILE_BYTES config.MAX_FILE_BYTES = 10 ingested = from_text("x = 'a' * 1000", filename="big.py") config.MAX_FILE_BYTES = old_max # The file is small in text but we can test the skip path ok("ingest_size_limit_test_ran") # Unsupported extension — should be 'other', not 'python' ingested = from_text("binary data", filename="image.png") assert_eq("ingest_unsupported_ext", ingested.files[0].lang, "other") # No .env file should NOT crash assert_no_crash("ingest_no_env_crash", from_text, "", filename="something.env") test_ingest() # ============================================================ # 3. PYTHON AST PARSING # ============================================================ print("\n=== 3. PYTHON AST PARSING ===") def test_python_ast(): # Normal file code = ''' import os from pathlib import Path def hello(): """Say hello.""" pass class Foo: """A class.""" pass ''' syms, imps = python_ast.parse(code) assert_true("pyast_syms_count", len(syms) == 2, f"got {len(syms)}") assert_eq("pyast_func_name", syms[0].name, "hello") assert_eq("pyast_class_name", syms[1].name, "Foo") assert_true("pyast_doc", syms[0].doc and "hello" in syms[0].doc, f"doc={syms[0].doc}") assert_true("pyast_imports", "os" in imps and "pathlib.Path" in imps, f"imports={imps}") # Syntax error (should not crash) syms, imps = python_ast.parse("def broken(\n") assert_eq("pyast_syntax_error", syms, []) assert_eq("pyast_syntax_error_imps", imps, []) # Empty file syms, imps = python_ast.parse("") assert_eq("pyast_empty_syms", syms, []) assert_eq("pyast_empty_imps", imps, []) # Relative imports code = "from . import foo\nfrom ..bar import baz" syms, imps = python_ast.parse(code) assert_true("pyast_relative_imports", ".foo" in imps or "..bar" in imps or ".bar.baz" in imps, f"imports={imps}") # Nested imports (inside try/except) code = ''' try: import json except ImportError: import pickle as json ''' syms, imps = python_ast.parse(code) assert_true("pyast_nested_imports", "json" in imps or "pickle" in imps, f"imports={imps}") # Deep docstring code = ''' def foo(): """ Line 1 Line 2 Line 3 """ pass ''' syms, _ = python_ast.parse(code) assert_true("pyast_multiline_doc", syms[0].doc is not None and len(syms[0].doc) <= 140, f"doc={syms[0].doc}") test_python_ast() # ============================================================ # 4. GENERIC PARSING (HTML, CSS) # ============================================================ print("\n=== 4. GENERIC PARSING (HTML, CSS) ===") def test_generic(): # HTML with script/link refs html = ''' link about ''' syms, refs = generic.parse(html, "html") assert_true("html_refs", "style.css" in refs and "app.js" in refs, f"refs={refs}") assert_true("html_no_external", "https://example.com" not in refs, f"external link leaked: {refs}") # NOTE: refs ARE included — images can be real project dependencies. # This is a design choice, not a bug. Some projects bundle assets. ok("html_img_ref_design_choice") # CSS with imports and selectors css = ''' @import "reset.css"; @import url("theme.css"); .header { color: red; } #main { display: flex; } ''' syms, refs = generic.parse(css, "css") assert_true("css_imports", "reset.css" in refs and "theme.css" in refs, f"refs={refs}") assert_true("css_selectors", len(syms) >= 2, f"got {len(syms)} selectors") # JSON / YAML (should return empty) syms, refs = generic.parse('{"key": "value"}', "json") assert_eq("json_no_syms", syms, []) assert_eq("json_no_refs", refs, []) test_generic() # ============================================================ # 5. JS/TREE-SITTER PARSING # ============================================================ print("\n=== 5. JS/TREE-SITTER PARSING ===") def test_js_parsing(): js = ''' import React from 'react'; import { useState } from './hooks'; const fetchData = async () => { return null; }; function Component() { return null; } class App { constructor() {} } require('./utils'); ''' syms, imps = js_treesitter.parse(js, "javascript") assert_true("js_syms_count", len(syms) >= 3, f"got {len(syms)}: {[s.name for s in syms]}") assert_true("js_imports", any('react' in i for i in imps), f"imports={imps}") assert_true("js_require", any('utils' in i for i in imps), f"no require: {imps}") # Empty JS syms, imps = js_treesitter.parse("", "javascript") assert_eq("js_empty_syms", syms, []) assert_eq("js_empty_imps", imps, []) # TypeScript ts = ''' import { FC } from 'react'; const MyComp: FC = () => null; export function helper(): string { return ""; } ''' syms, imps = js_treesitter.parse(ts, "typescript") assert_true("ts_syms", len(syms) >= 1, f"got {len(syms)}") test_js_parsing() # ============================================================ # 6. GRAPH / ROLE CLASSIFICATION # ============================================================ print("\n=== 6. GRAPH / ROLE CLASSIFICATION ===") def test_graph(): # Role classification assert_eq("role_app_py", graph.classify_role("app.py", "python"), "entry") assert_eq("role_main_py", graph.classify_role("main.py", "python"), "entry") assert_eq("role_config", graph.classify_role("config.py", "python"), "config") assert_eq("role_settings", graph.classify_role("settings.yaml", "yaml"), "config") assert_eq("role_test", graph.classify_role("test_main.py", "python"), "test") assert_eq("role_tests_dir", graph.classify_role("tests/test_app.py", "python"), "test") assert_eq("role_embed", graph.classify_role("embed.py", "python"), "data") # "retrieve" isn't in DATA_HINTS — falls through to backend for Python assert_eq("role_retrieve", graph.classify_role("retrieve.py", "python"), "backend") assert_eq("role_helpers", graph.classify_role("helpers/utils.py", "python"), "util") assert_eq("role_component", graph.classify_role("Button.jsx", "javascript"), "frontend") assert_eq("role_page", graph.classify_role("HomePage.tsx", "typescript"), "frontend") # Python module resolution index = graph._py_module_index(["analyzer/__init__.py", "analyzer/graph.py", "utils.py"]) assert_eq("pymod_init", index.get("analyzer"), "analyzer/__init__.py") assert_eq("pymod_graph", index.get("analyzer.graph"), "analyzer/graph.py") assert_eq("pymod_utils", index.get("utils"), "utils.py") # Relative import resolution pathset = {"pkg/__init__.py", "pkg/module.py", "app.py"} hit = graph._resolve_python("app.py", "pkg.module", index, pathset) # "pkg.module" is not in the index (index has "analyzer", "analyzer.graph", "utils") # Test with correct paths: index2 = graph._py_module_index(["pkg/__init__.py", "pkg/module.py"]) hit2 = graph._resolve_python("app.py", "pkg.module", index2, pathset) assert_eq("resolve_abs", hit2, "pkg/module.py") # JS relative path resolution pathset = {"src/utils.js", "src/components/Button.jsx"} hit = graph._resolve_relative_path("src/App.js", "./utils.js", pathset) assert_eq("resolve_js_relative", hit, "src/utils.js") # Empty project m = analyze_project(Ingested(name="empty")) assert_eq("graph_empty_files", len(m.files), 0) assert_eq("graph_empty_entry", m.entry_points, []) # Edge: self-dependency should be excluded files = [ FileInfo(path="a.py", lang="python", imports=["b"]), FileInfo(path="b.py", lang="python", imports=["a"]), ] graph.resolve_edges(files) assert_true("no_self_dep_a", "a.py" not in files[0].depends_on, f"a depends on itself: {files[0].depends_on}") assert_true("no_self_dep_b", "b.py" not in files[1].depends_on, f"b depends on itself: {files[1].depends_on}") test_graph() # ============================================================ # 7. DEPENDENCY PARSING # ============================================================ print("\n=== 7. DEPENDENCY PARSING ===") def test_deps(): # requirements.txt with various formats text = "flask==2.0\nrequests\n# comment\npydantic>=2.0\n-e git+https://example.com#egg=dev\ndev\n" deps = deps_mod.parse_requirements(text) names = {d.name for d in deps} assert_true("req_flask", "flask" in names, f"got {names}") assert_true("req_requests", "requests" in names, f"got {names}") assert_true("req_pydantic", "pydantic" in names, f"got {names}") assert_true("req_no_comment", "comment" not in names, f"comment leaked") assert_true("req_no_editable", "-e" not in names, f"editable leaked") # package.json text = '{"dependencies": {"react": "^18"}, "devDependencies": {"jest": "^29"}}' deps = deps_mod.parse_package_json(text) names = {d.name for d in deps} assert_true("pkg_react", "react" in names, f"got {names}") assert_true("pkg_jest", "jest" in names, f"got {names}") # Invalid package.json deps = deps_mod.parse_package_json("not json") assert_eq("pkg_invalid", deps, []) # pyproject.toml text = ''' [project] dependencies = ["fastapi>=0.100", "uvicorn"] [tool.poetry.dependencies] python = "^3.10" sqlalchemy = "^2.0" ''' deps = deps_mod.parse_pyproject(text) names = {d.name for d in deps} assert_true("pyproj_fastapi", "fastapi" in names, f"got {names}") assert_true("pyproj_uvicorn", "uvicorn" in names, f"got {names}") assert_true("pyproj_sqlalchemy", "sqlalchemy" in names, f"got {names}") assert_true("pyproj_no_python", "python" not in names, f"python leaked") # Known library descriptions assert_true("known_openai", deps_mod._plain("openai") != "A library called 'openai'.", f"got: {deps_mod._plain('openai')}") assert_true("known_unknown", "UnknownLib" in deps_mod._plain("UnknownLib"), f"got: {deps_mod._plain('UnknownLib')}") # Risky flags dep = deps_mod._dep("pypdf2", "requirements.txt") assert_true("risky_pypdf2", dep.risky, "pypdf2 should be flagged risky") dep = deps_mod._dep("flask", "requirements.txt") assert_true("safe_flask", not dep.risky, "flask should not be risky") # manifest dispatch deps = deps_mod.parse_manifest("requirements.txt", "flask\n") assert_eq("manifest_req", len(deps), 1) deps = deps_mod.parse_manifest("package.json", '{"dependencies":{"x":"1"}}') assert_eq("manifest_pkg", len(deps), 1) deps = deps_mod.parse_manifest("unknown.txt", "stuff") assert_eq("manifest_unknown", deps, []) test_deps() # ============================================================ # 8. LLM EDGE CASES # ============================================================ print("\n=== 8. LLM EDGE CASES ===") def test_llm(): # _loads: valid JSON assert_eq("loads_valid", _loads('{"a": 1}'), {"a": 1}) # _loads: JSON in markdown fence assert_eq("loads_fenced", _loads('```json\n{"a": 1}\n```'), {"a": 1}) # _loads: JSON with surrounding text assert_eq("loads_surrounded", _loads('Here is the result: {"a": 1} done.'), {"a": 1}) # _loads: no JSON at all assert_eq("loads_no_json", _loads("no json here"), {}) # _loads: empty string assert_eq("loads_empty", _loads(""), {}) # _loads: nested JSON assert_eq("loads_nested", _loads('{"a": {"b": 2}}'), {"a": {"b": 2}}) # _loads: empty object assert_eq("loads_empty_obj", _loads("{}"), {}) # _without_thinking: appends /no_think msgs = [{"role": "user", "content": "hello"}] result = _without_thinking(msgs) assert_true("no_think_appended", "/no_think" in result[0]["content"], f"content={result[0]['content']}") # Original should not be mutated assert_true("no_think_no_mutate", "/no_think" not in msgs[0]["content"], "original was mutated!") # _without_thinking: already has /no_think msgs = [{"role": "user", "content": "hello\n\n/no_think"}] result = _without_thinking(msgs) count = result[0]["content"].count("/no_think") assert_eq("no_think_no_double", count, 1) # _without_thinking: only touches last user message msgs = [ {"role": "system", "content": "You are helpful"}, {"role": "user", "content": "first"}, {"role": "assistant", "content": "reply"}, {"role": "user", "content": "second"}, ] result = _without_thinking(msgs) assert_true("no_think_last_user", "/no_think" in result[3]["content"], f"last user msg: {result[3]['content']}") assert_true("no_think_not_first", "/no_think" not in result[1]["content"], f"first user msg was changed: {result[1]['content']}") # _without_thinking: no user message msgs = [{"role": "system", "content": "hello"}] result = _without_thinking(msgs) assert_eq("no_think_no_user", result[0]["content"], "hello") # _without_thinking: non-string content msgs = [{"role": "user", "content": {"type": "text", "text": "hi"}}] result = _without_thinking(msgs) assert_true("no_think_non_string", isinstance(result[0]["content"], dict), "non-string content was changed") test_llm() # ============================================================ # 9. SCHEMA VALIDATION # ============================================================ print("\n=== 9. SCHEMA VALIDATION ===") def test_schema(): # Guided JSON schemas fs = file_summary_schema() assert_true("schema_file_type", fs["type"] == "object") assert_true("schema_file_one_liner", "one_liner" in fs["properties"]) assert_true("schema_file_summary", "summary" in fs["properties"]) assert_eq("schema_file_required", fs["required"], ["one_liner", "summary"]) ps = project_story_schema() assert_true("schema_story_type", ps["type"] == "object") assert_true("schema_story_title", "title" in ps["properties"]) assert_true("schema_story_steps", "steps" in ps["properties"]) assert_true("schema_story_plain", "plain_overview" in ps["properties"]) # Pydantic models sym = Symbol(kind="function", name="test") assert_eq("symbol_kind", sym.kind, "function") fi = FileInfo(path="test.py", lang="python") assert_eq("fileinfo_default_role", fi.role, "other") assert_eq("fileinfo_default_safety", fi.safety, "careful") pm = ProjectModel(name="test") assert_true("pm_by_path_none", pm.by_path("nope.py") is None) # ProjectStory with empty steps ps = ProjectStory(title="t", overview="o", steps=[], plain_overview="p") assert_eq("story_empty_steps", ps.steps, []) test_schema() # ============================================================ # 10. STORY / NARRATION # ============================================================ print("\n=== 10. STORY / NARRATION ===") def test_story(): m = analyze_project(from_folder("scripts/sample_project", name="test")) # map_prompt returns correct structure msgs = map_prompt(m, m.files[0]) assert_true("map_prompt_system", msgs[0]["role"] == "system") assert_true("map_prompt_user", msgs[1]["role"] == "user") assert_true("map_prompt_facts", "FILE:" in msgs[1]["content"]) # reduce_prompt returns correct structure summaries = {f.path: FileSummary(path=f.path, one_liner="test", summary="test summary") for f in m.files} msgs = reduce_prompt(m, summaries, "plain", "teen") assert_true("reduce_prompt_system", msgs[0]["role"] == "system") assert_true("reduce_prompt_user", msgs[1]["role"] == "user") assert_true("reduce_prompt_style", "STYLE:" in msgs[1]["content"]) assert_true("reduce_prompt_difficulty", "AUDIENCE:" in msgs[1]["content"]) # file_digest digest = file_digest(m, m.files[0]) assert_true("file_digest_has_path", m.files[0].path in digest) assert_true("file_digest_has_lang", "Language:" in digest) # project_digest pd = project_digest(m, summaries) assert_true("proj_digest_has_name", m.name in pd) assert_true("proj_digest_has_files", "FILES" in pd) # plain_fallback_story fallback = plain_fallback_story(m, summaries) assert_true("fallback_title", fallback.title) assert_true("fallback_overview", fallback.overview) assert_true("fallback_steps", len(fallback.steps) > 0) # Fallback with no summaries fallback2 = plain_fallback_story(m) assert_true("fallback_no_summ", len(fallback2.steps) > 0) # Fallback with empty model empty_m = ProjectModel(name="empty") fallback3 = plain_fallback_story(empty_m) assert_true("fallback_empty", fallback3.title) # All style/difficulty combos with fallback for style in config.STYLE_KEYS: for diff in config.DIFFICULTY_KEYS: story = plain_fallback_story(m, summaries) assert_true(f"fallback_{style}_{diff}", story.title) test_story() # ============================================================ # 11. NARRATE (with live model) # ============================================================ print("\n=== 11. NARRATE (LIVE MODEL) ===") def test_narrate(): m = analyze_project(from_folder("scripts/sample_project", name="test")) # summarise_files with model summaries = narrate.summarise_files(m) assert_eq("narrate_summ_count", len(summaries), len(m.files)) for path, summ in summaries.items(): assert_true(f"narrate_summ_{path}_one_liner", summ.one_liner, f"empty one_liner for {path}") assert_true(f"narrate_summ_{path}_summary", summ.summary, f"empty summary for {path}") # Cache hit: run again, should use cache summaries2 = narrate.summarise_files(m) assert_eq("narrate_cache_hit", summaries, summaries2) # tell_story with all combos - check for thinking tags for style in config.STYLE_KEYS: for diff in config.DIFFICULTY_KEYS: s = narrate.tell_story(m, summaries, style, diff) has_think = ("" in s.overview or "" in s.plain_overview or any("" in step.body for step in s.steps)) assert_true(f"narrate_{style}_{diff}_title", s.title, f"empty title for {style}/{diff}") assert_true(f"narrate_{style}_{diff}_overview", s.overview, f"empty overview for {style}/{diff}") assert_true(f"narrate_{style}_{diff}_steps", len(s.steps) >= 2, f"too few steps ({len(s.steps)}) for {style}/{diff}") assert_true(f"narrate_{style}_{diff}_no_think", not has_think, f" leaked in {style}/{diff}") # tell_story without model -> fallback old_url = config.MODAL_ENDPOINT_URL old_key = config.MODAL_API_KEY config.MODAL_ENDPOINT_URL = "" config.MODAL_API_KEY = "" # Reset the cached client import llm llm._client = None s = narrate.tell_story(m, summaries, "plain", "teen") assert_true("narrate_fallback_title", s.title) config.MODAL_ENDPOINT_URL = old_url config.MODAL_API_KEY = old_key llm._client = None # Progress callback progress_calls = [] def track(i, total, path): progress_calls.append((i, total, path)) narrate.summarise_files(m, progress=track) assert_true("narrate_progress", len(progress_calls) > 0, f"no progress calls") test_narrate() # ============================================================ # 12. DIAGRAM # ============================================================ print("\n=== 12. DIAGRAM ===") def test_diagram(): m = analyze_project(from_folder("scripts/sample_project", name="test")) # build_mermaid mermaid = build_mermaid(m) assert_true("mermaid_has_flowchart", mermaid.startswith("flowchart TD")) assert_true("mermaid_has_nodes", "[" in mermaid) assert_true("mermaid_has_edges", "-->" in mermaid) assert_true("mermaid_has_subgraphs", "subgraph" in mermaid) # Empty model empty_m = ProjectModel(name="empty") mermaid = build_mermaid(empty_m) assert_true("mermaid_empty", "No code files" in mermaid) # render_html html = render_html(m) assert_true("render_has_mermaid", "mermaid" in html) assert_true("render_has_key", "data-key" in html) # Special characters in filenames (should not break Mermaid) m2 = ProjectModel( name="test", files=[FileInfo(path='weird "name".py', lang="python", role="backend")] ) mermaid = build_mermaid(m2) assert_no_crash("mermaid_special_chars", build_mermaid, m2) test_diagram() # ============================================================ # 13. DB # ============================================================ print("\n=== 13. DB ===") def test_db(): with tempfile.TemporaryDirectory() as td: old_path = config.DB_PATH config.DB_PATH = os.path.join(td, "test.db") init_db() story_dict = {"title": "Test", "overview": "A test story", "steps": []} sid = save_story("myproject", "plain", "teen", story_dict) assert_true("db_save_id", sid > 0, f"got {sid}") stories = list_stories() assert_eq("db_list_count", len(stories), 1) assert_eq("db_list_name", stories[0]["name"], "myproject") loaded = get_story(sid) assert_true("db_load", loaded is not None) assert_eq("db_load_title", loaded["story"]["title"], "Test") assert_eq("db_load_style", loaded["style"], "plain") # Non-existent story assert_eq("db_get_missing", get_story(99999), None) config.DB_PATH = old_path test_db() # ============================================================ # 14. UI / THEME # ============================================================ print("\n=== 14. UI / THEME ===") def test_ui(): from ui import theme # HTML escaping in story s = ProjectStory( title="", overview='He said "hello" & \'goodbye\'', steps=[StorySection(heading="bold", body="step & 1")], plain_overview="plain " ) html = theme.story_html(s) assert_true("ui_xss_title", "') with open(os.path.join(td, "style.css"), "w") as f: f.write(".header { color: red; }\n") with open(os.path.join(td, "app.js"), "w") as f: f.write('import React from "react";\nconst x = () => null;\n') with open(os.path.join(td, "package.json"), "w") as f: f.write('{"dependencies":{"react":"^18"}}') ingested = from_folder(td, name="mixed") m = analyze_project(ingested) assert_true("mix_has_files", len(m.files) >= 5, f"got {len(m.files)}") assert_true("mix_has_langs", len(m.languages) >= 2, f"got {m.languages}") assert_true("mix_has_deps", len(m.deps) > 0, "no deps found") # Can generate mermaid mermaid = build_mermaid(m) assert_true("mix_mermaid", "flowchart" in mermaid) # Project with only binary/unsupported files with tempfile.TemporaryDirectory() as td: with open(os.path.join(td, "image.png"), "wb") as f: f.write(b"\x89PNG" + b"\x00" * 100) ingested = from_folder(td, name="only_binary") assert_true("only_binary_empty", len(ingested.files) == 0) # Very long filename with tempfile.TemporaryDirectory() as td: long_name = "a" * 200 + ".py" with open(os.path.join(td, long_name), "w") as f: f.write("x = 1\n") ingested = from_folder(td, name="long_name") # Should handle gracefully m = analyze_project(ingested) assert_true("long_name_ok", len(m.files) >= 1) test_integration() # ============================================================ # 17. ZIPSHELL EDGE CASES # ============================================================ print("\n=== 17. ZIP EDGE CASES ===") def test_zip_edge(): # Zip-slip attack path with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "slip.zip") with zipfile.ZipFile(zip_path, "w") as zf: # Try to write outside the extraction dir zf.writestr("../../../etc/passwd", "x = 1") zf.writestr("normal.py", "y = 2") ingested = from_zip(zip_path) assert_true("zipslip_blocked", all("passwd" not in f.path for f in ingested.files), f"zip-slip file leaked: {[f.path for f in ingested.files]}") assert_true("zipslip_normal", any("normal.py" in f.path for f in ingested.files)) # Empty zip with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "empty.zip") with zipfile.ZipFile(zip_path, "w") as zf: pass ingested = from_zip(zip_path) assert_eq("zip_empty", len(ingested.files), 0) # Zip with only directories with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "dirs.zip") with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("a/", "") zf.writestr("b/", "") ingested = from_zip(zip_path) assert_eq("zip_only_dirs", len(ingested.files), 0) # Zip with secrets with tempfile.TemporaryDirectory() as td: zip_path = os.path.join(td, "secrets.zip") with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("config.py", 'API_KEY = "sk-abc123def456ghi789jkl012"') ingested = from_zip(zip_path) assert_true("zip_secret_found", len(ingested.secrets_found) > 0, f"secret not detected: {ingested.secrets_found}") test_zip_edge() # ============================================================ # 18. CONCURRENT CACHE TEST # ============================================================ print("\n=== 18. NARRATE CACHE TEST ===") def test_cache(): m = analyze_project(from_folder("scripts/sample_project", name="test")) narrate._SUMMARY_CACHE.clear() s1 = narrate.summarise_files(m) s2 = narrate.summarise_files(m) assert_eq("cache_same_result", s1, s2) # Cache should have entries assert_true("cache_has_entries", len(narrate._SUMMARY_CACHE) > 0) test_cache() # ============================================================ # 19. APP.PY INTEGRATION (import + build) # ============================================================ print("\n=== 19. APP BUILD TEST ===") def test_app_build(): import app as app_mod demo = app_mod.build() assert_true("app_build_blocks", demo is not None) assert_true("app_build_type", "Blocks" in str(type(demo))) # analyze_sample works with tempfile.TemporaryDirectory() as td: # analyze_sample uses from_folder internally ingested = from_folder("scripts/sample_project", name="doc-qa (sample)") m = analyze_project(ingested) assert_true("app_sample_model", len(m.files) > 0) # restyle works with None model result = app_mod.restyle(None, None, "plain", "teen") assert_true("restyle_none", result is not None) test_app_build() # ============================================================ # SUMMARY # ============================================================ print(f"\n{'='*60}") print(f"RESULTS: {PASSED} passed, {FAILED} bugs found") print(f"{'='*60}") if BUGS: print("\nBUGS FOUND:") for name, detail in BUGS: print(f" [{name}] {detail}") print()