"""Tests for the plugin `src/` module-serving route and the live-edit cache contract added in R0 (module-migration rails). Covers: * GET /api/plugins/{id}/src/{path} serves a plugin's ES-module source tree with the right Content-Type, including nested paths. * Path containment: `..`, absolute, and NUL are rejected (404) — the same `safe_join` guard the assets/ route uses. * The live-edit cache contract: no-cache + a weak ETag, a bodyless 304 on matching If-None-Match, and no stale 304 after an in-place edit. * screen.js and assets/ now also emit an ETag and honor If-None-Match (previously screen.js sent no headers and assets/ never returned 304). The routes read the module-global `plugins.LOADED_PLUGINS`, so each test registers a fake ready plugin directly (save/restore that global) and drives `register_plugin_api` on a fresh FastAPI app — no full server import needed. """ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient import plugins PLUGIN_ID = "srctest" @pytest.fixture() def client(tmp_path): """A TestClient with `register_plugin_api` wired and a single fake ready plugin whose dir (`tmp_path`) holds a src/ tree, an asset, and a screen.js. Restores LOADED_PLUGINS afterward.""" (tmp_path / "src").mkdir() (tmp_path / "src" / "main.js").write_text("import './util/x.js';\nexport const boot = 1;\n") (tmp_path / "src" / "util").mkdir() (tmp_path / "src" / "util" / "x.js").write_text("export const x = 42;\n") (tmp_path / "src" / "theme.css").write_text(".a{color:red}\n") (tmp_path / "assets").mkdir() (tmp_path / "assets" / "worklet.js").write_text("// worklet\n") (tmp_path / "screen.js").write_text("import './src/main.js';\n") saved = list(plugins.LOADED_PLUGINS) plugins.LOADED_PLUGINS.clear() plugins.LOADED_PLUGINS.append({ "id": PLUGIN_ID, "status": "ready", "_dir": tmp_path, "_manifest": {"script": "screen.js", "scriptType": "module"}, }) app = FastAPI() plugins.register_plugin_api(app) c = TestClient(app, raise_server_exceptions=True) try: yield c, tmp_path finally: c.close() plugins.LOADED_PLUGINS.clear() plugins.LOADED_PLUGINS.extend(saved) def test_src_file_served_with_js_media_type(client): c, _ = client r = c.get(f"/api/plugins/{PLUGIN_ID}/src/main.js") assert r.status_code == 200 # Either application/javascript or text/javascript is a valid module-script # MIME (guess_type returns text/javascript on newer platforms); browsers # accept both for