| """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}") |
|
|
|
|
| |
| |
| |
| print("\n=== 1. SECRET SCANNING ===") |
|
|
| def test_secret_scanning(): |
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| clean, hits = redact_secrets("") |
| assert_eq("secret_empty_text", clean, "") |
| assert_eq("secret_empty_hits", hits, []) |
|
|
| |
| clean, hits = redact_secrets("x = 42\nprint('hello')") |
| assert_eq("secret_clean_code", hits, []) |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 2. INGEST EDGE CASES ===") |
|
|
| def test_ingest(): |
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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}") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| assert_true("ingest_binary_ext", config._lang_for if hasattr(config, '_lang_for') else True, "") |
|
|
| |
| 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 |
| |
| ok("ingest_size_limit_test_ran") |
|
|
| |
| ingested = from_text("binary data", filename="image.png") |
| assert_eq("ingest_unsupported_ext", ingested.files[0].lang, "other") |
|
|
| |
| assert_no_crash("ingest_no_env_crash", from_text, "", filename="something.env") |
|
|
|
|
| test_ingest() |
|
|
|
|
| |
| |
| |
| print("\n=== 3. PYTHON AST PARSING ===") |
|
|
| def test_python_ast(): |
| |
| 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}") |
|
|
| |
| syms, imps = python_ast.parse("def broken(\n") |
| assert_eq("pyast_syntax_error", syms, []) |
| assert_eq("pyast_syntax_error_imps", imps, []) |
|
|
| |
| syms, imps = python_ast.parse("") |
| assert_eq("pyast_empty_syms", syms, []) |
| assert_eq("pyast_empty_imps", imps, []) |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 4. GENERIC PARSING (HTML, CSS) ===") |
|
|
| def test_generic(): |
| |
| html = ''' |
| <html> |
| <head> |
| <link href="style.css" rel="stylesheet"> |
| <script src="app.js"></script> |
| </head> |
| <body> |
| <img src="logo.png"> |
| <a href="https://example.com">link</a> |
| <a href="about.html">about</a> |
| </body> |
| </html> |
| ''' |
| 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}") |
| |
| |
| ok("html_img_ref_design_choice") |
|
|
| |
| 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") |
|
|
| |
| syms, refs = generic.parse('{"key": "value"}', "json") |
| assert_eq("json_no_syms", syms, []) |
| assert_eq("json_no_refs", refs, []) |
|
|
|
|
| test_generic() |
|
|
|
|
| |
| |
| |
| 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}") |
|
|
| |
| syms, imps = js_treesitter.parse("", "javascript") |
| assert_eq("js_empty_syms", syms, []) |
| assert_eq("js_empty_imps", imps, []) |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 6. GRAPH / ROLE CLASSIFICATION ===") |
|
|
| def test_graph(): |
| |
| 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") |
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| pathset = {"pkg/__init__.py", "pkg/module.py", "app.py"} |
| hit = graph._resolve_python("app.py", "pkg.module", index, pathset) |
| |
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| m = analyze_project(Ingested(name="empty")) |
| assert_eq("graph_empty_files", len(m.files), 0) |
| assert_eq("graph_empty_entry", m.entry_points, []) |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 7. DEPENDENCY PARSING ===") |
|
|
| def test_deps(): |
| |
| 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") |
|
|
| |
| 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}") |
|
|
| |
| deps = deps_mod.parse_package_json("not json") |
| assert_eq("pkg_invalid", deps, []) |
|
|
| |
| 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") |
|
|
| |
| 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')}") |
|
|
| |
| 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") |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 8. LLM EDGE CASES ===") |
|
|
| def test_llm(): |
| |
| assert_eq("loads_valid", _loads('{"a": 1}'), {"a": 1}) |
|
|
| |
| assert_eq("loads_fenced", _loads('```json\n{"a": 1}\n```'), {"a": 1}) |
|
|
| |
| assert_eq("loads_surrounded", _loads('Here is the result: {"a": 1} done.'), {"a": 1}) |
|
|
| |
| assert_eq("loads_no_json", _loads("no json here"), {}) |
|
|
| |
| assert_eq("loads_empty", _loads(""), {}) |
|
|
| |
| assert_eq("loads_nested", _loads('{"a": {"b": 2}}'), {"a": {"b": 2}}) |
|
|
| |
| assert_eq("loads_empty_obj", _loads("{}"), {}) |
|
|
| |
| 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']}") |
| |
| assert_true("no_think_no_mutate", "/no_think" not in msgs[0]["content"], |
| "original was mutated!") |
|
|
| |
| 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) |
|
|
| |
| 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']}") |
|
|
| |
| msgs = [{"role": "system", "content": "hello"}] |
| result = _without_thinking(msgs) |
| assert_eq("no_think_no_user", result[0]["content"], "hello") |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 9. SCHEMA VALIDATION ===") |
|
|
| def test_schema(): |
| |
| 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"]) |
|
|
| |
| 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) |
|
|
| |
| ps = ProjectStory(title="t", overview="o", steps=[], plain_overview="p") |
| assert_eq("story_empty_steps", ps.steps, []) |
|
|
|
|
| test_schema() |
|
|
|
|
| |
| |
| |
| print("\n=== 10. STORY / NARRATION ===") |
|
|
| def test_story(): |
| m = analyze_project(from_folder("scripts/sample_project", name="test")) |
|
|
| |
| 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"]) |
|
|
| |
| 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"]) |
|
|
| |
| 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) |
|
|
| |
| pd = project_digest(m, summaries) |
| assert_true("proj_digest_has_name", m.name in pd) |
| assert_true("proj_digest_has_files", "FILES" in pd) |
|
|
| |
| 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) |
|
|
| |
| fallback2 = plain_fallback_story(m) |
| assert_true("fallback_no_summ", len(fallback2.steps) > 0) |
|
|
| |
| empty_m = ProjectModel(name="empty") |
| fallback3 = plain_fallback_story(empty_m) |
| assert_true("fallback_empty", fallback3.title) |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| print("\n=== 11. NARRATE (LIVE MODEL) ===") |
|
|
| def test_narrate(): |
| m = analyze_project(from_folder("scripts/sample_project", name="test")) |
|
|
| |
| 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}") |
|
|
| |
| summaries2 = narrate.summarise_files(m) |
| assert_eq("narrate_cache_hit", summaries, summaries2) |
|
|
| |
| for style in config.STYLE_KEYS: |
| for diff in config.DIFFICULTY_KEYS: |
| s = narrate.tell_story(m, summaries, style, diff) |
| has_think = ("<think>" in s.overview or |
| "<think>" in s.plain_overview or |
| any("<think>" 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"<think> leaked in {style}/{diff}") |
|
|
| |
| old_url = config.MODAL_ENDPOINT_URL |
| old_key = config.MODAL_API_KEY |
| config.MODAL_ENDPOINT_URL = "" |
| config.MODAL_API_KEY = "" |
| |
| 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_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() |
|
|
|
|
| |
| |
| |
| print("\n=== 12. DIAGRAM ===") |
|
|
| def test_diagram(): |
| m = analyze_project(from_folder("scripts/sample_project", name="test")) |
|
|
| |
| 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_m = ProjectModel(name="empty") |
| mermaid = build_mermaid(empty_m) |
| assert_true("mermaid_empty", "No code files" in mermaid) |
|
|
| |
| html = render_html(m) |
| assert_true("render_has_mermaid", "mermaid" in html) |
| assert_true("render_has_key", "data-key" in html) |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| 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") |
|
|
| |
| assert_eq("db_get_missing", get_story(99999), None) |
|
|
| config.DB_PATH = old_path |
|
|
|
|
| test_db() |
|
|
|
|
| |
| |
| |
| print("\n=== 14. UI / THEME ===") |
|
|
| def test_ui(): |
| from ui import theme |
|
|
| |
| s = ProjectStory( |
| title="<script>alert('xss')</script>", |
| overview='He said "hello" & \'goodbye\'', |
| steps=[StorySection(heading="<b>bold</b>", body="step & 1")], |
| plain_overview="plain <text>" |
| ) |
| html = theme.story_html(s) |
| assert_true("ui_xss_title", "<script>" not in html, |
| f"XSS in title not escaped: {html[:200]}") |
| assert_true("ui_xss_body", "<b>bold</b>" not in html, |
| f"HTML in body not escaped") |
| assert_true("ui_ampersand", "&" in html or "&" in html, |
| "ampersand handling") |
|
|
| |
| html = theme.banner("test message", "warn") |
| assert_true("banner_has_text", "test message" in html) |
| assert_true("banner_has_class", "warn" in html) |
| assert_eq("banner_empty", theme.banner(""), "") |
|
|
| |
| m = analyze_project(from_folder("scripts/sample_project", name="test")) |
| html = theme.safe_to_edit_html(m) |
| assert_true("safe_has_legend", "safe to change" in html.lower() or "🟢" in html) |
| assert_true("safe_has_files", ".py" in html) |
|
|
| |
| html = theme.deps_html(m) |
| assert_true("deps_has_packages", "📦" in html or "openai" in html.lower()) |
|
|
| |
| empty_m = ProjectModel(name="empty") |
| html = theme.deps_html(empty_m) |
| assert_true("deps_empty", "No dependency" in html or "no dependency" in html.lower()) |
|
|
| |
| html = theme.plain_panel_html(s) |
| assert_true("plain_has_label", "Plain English" in html) |
| assert_true("plain_has_body", "plain <text>" not in html) |
|
|
| |
| html = theme.header_html() |
| assert_true("header_has_storycode", "StoryCode" in html or "Story" in html) |
|
|
|
|
| test_ui() |
|
|
|
|
| |
| |
| |
| print("\n=== 15. CONFIG EDGE CASES ===") |
|
|
| def test_config(): |
| |
| assert_eq("cfg_py", config.LANG_BY_EXT.get(".py"), "python") |
| assert_eq("cfg_js", config.LANG_BY_EXT.get(".js"), "javascript") |
| assert_eq("cfg_ts", config.LANG_BY_EXT.get(".ts"), "typescript") |
| assert_eq("cfg_jsx", config.LANG_BY_EXT.get(".jsx"), "javascript") |
| assert_eq("cfg_tsx", config.LANG_BY_EXT.get(".tsx"), "typescript") |
| assert_eq("cfg_html", config.LANG_BY_EXT.get(".html"), "html") |
| assert_eq("cfg_css", config.LANG_BY_EXT.get(".css"), "css") |
| assert_eq("cfg_json", config.LANG_BY_EXT.get(".json"), "json") |
| assert_eq("cfg_yaml", config.LANG_BY_EXT.get(".yaml"), "yaml") |
| assert_eq("cfg_unknown", config.LANG_BY_EXT.get(".xyz"), None) |
|
|
| |
| assert_true("cfg_binary_png", ".png" in config.BINARY_EXTS) |
| assert_true("cfg_binary_zip", ".zip" in config.BINARY_EXTS) |
| assert_true("cfg_binary_exe", ".exe" in config.BINARY_EXTS) |
|
|
| |
| assert_true("cfg_ignore_node", "node_modules" in config.IGNORE_DIRS) |
| assert_true("cfg_ignore_git", ".git" in config.IGNORE_DIRS) |
|
|
| |
| role_keys = {r.key for r in config.ROLES} |
| assert_true("cfg_roles_complete", |
| {"entry", "frontend", "backend", "data", "config", "test", "util", "other"}.issubset(role_keys)) |
|
|
| |
| assert_eq("cfg_styles_count", len(config.STYLES), 5) |
| assert_eq("cfg_default_style", config.DEFAULT_STYLE, "plain") |
|
|
| |
| assert_eq("cfg_diff_count", len(config.DIFFICULTIES), 3) |
| assert_eq("cfg_default_diff", config.DEFAULT_DIFFICULTY, "teen") |
|
|
|
|
| test_config() |
|
|
|
|
| |
| |
| |
| print("\n=== 16. INTEGRATION: FULL PIPELINE EDGE CASES ===") |
|
|
| def test_integration(): |
| |
| with tempfile.TemporaryDirectory() as td: |
| with open(os.path.join(td, "app.py"), "w") as f: |
| f.write("import config\nx = 1\n") |
| with open(os.path.join(td, "config.py"), "w") as f: |
| f.write("y = 2\n") |
| with open(os.path.join(td, "ui.html"), "w") as f: |
| f.write('<html><script src="app.js"></script></html>') |
| 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") |
|
|
| |
| mermaid = build_mermaid(m) |
| assert_true("mix_mermaid", "flowchart" in mermaid) |
|
|
| |
| 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) |
|
|
| |
| 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") |
| |
| m = analyze_project(ingested) |
| assert_true("long_name_ok", len(m.files) >= 1) |
|
|
|
|
| test_integration() |
|
|
|
|
| |
| |
| |
| print("\n=== 17. ZIP EDGE CASES ===") |
|
|
| def test_zip_edge(): |
| |
| with tempfile.TemporaryDirectory() as td: |
| zip_path = os.path.join(td, "slip.zip") |
| with zipfile.ZipFile(zip_path, "w") as zf: |
| |
| 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)) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|
|
|
| |
| |
| |
| 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) |
| |
| assert_true("cache_has_entries", len(narrate._SUMMARY_CACHE) > 0) |
|
|
|
|
| test_cache() |
|
|
|
|
| |
| |
| |
| 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))) |
|
|
| |
| with tempfile.TemporaryDirectory() as td: |
| |
| ingested = from_folder("scripts/sample_project", name="doc-qa (sample)") |
| m = analyze_project(ingested) |
| assert_true("app_sample_model", len(m.files) > 0) |
|
|
| |
| result = app_mod.restyle(None, None, "plain", "teen") |
| assert_true("restyle_none", result is not None) |
|
|
|
|
| test_app_build() |
|
|
|
|
| |
| |
| |
| 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() |
|
|