chore: merge origin/main into mintlify/af82bda4 and resolve conflicts
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPOSITORY_ROOT = ROOT.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from build.build import ( # noqa: E402
|
||||
build,
|
||||
bundle_drift,
|
||||
render_template,
|
||||
replace_output,
|
||||
)
|
||||
from build.validate import validate_bundle # noqa: E402
|
||||
|
||||
|
||||
def test_render_rejects_unknown_or_unresolved_tokens() -> None:
|
||||
with pytest.raises(ValueError, match="UNKNOWN"):
|
||||
render_template("run {{UNKNOWN}}", {})
|
||||
|
||||
|
||||
def test_build_replaces_only_the_requested_output(tmp_path: Path) -> None:
|
||||
staged = tmp_path / "staged"
|
||||
staged.mkdir()
|
||||
(staged / "plugin.json").write_text("{}", encoding="utf-8")
|
||||
output = tmp_path / "output"
|
||||
output.mkdir()
|
||||
(output / "stale.py").write_text("stale", encoding="utf-8")
|
||||
sibling = tmp_path / "keep.txt"
|
||||
sibling.write_text("keep", encoding="utf-8")
|
||||
|
||||
replace_output(staged, output)
|
||||
|
||||
assert not (output / "stale.py").exists()
|
||||
assert (output / "plugin.json").exists()
|
||||
assert sibling.read_text(encoding="utf-8") == "keep"
|
||||
|
||||
|
||||
def test_build_cannot_replace_an_installable_source_directory(tmp_path: Path) -> None:
|
||||
staged = tmp_path / "staged"
|
||||
staged.mkdir()
|
||||
|
||||
with pytest.raises(ValueError, match="protected output path"):
|
||||
replace_output(staged, REPOSITORY_ROOT / "integrations" / "claude-code-plugin")
|
||||
|
||||
|
||||
def test_portable_bundle_is_conformant_and_self_contained(tmp_path: Path) -> None:
|
||||
root = build("mem0-agent-plugin", "portable", tmp_path / "mem0-agent-plugin")
|
||||
|
||||
assert validate_bundle(root, "portable") == []
|
||||
assert json.loads((root / "plugin.json").read_text(encoding="utf-8"))["$schema"] == (
|
||||
"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
|
||||
)
|
||||
server = json.loads((root / "mcp.json").read_text(encoding="utf-8"))["mcpServers"]["mem0"]
|
||||
assert server["type"] == "stdio"
|
||||
assert server["args"] == ["${PLUGIN_ROOT}/core/mcp_server.py"]
|
||||
assert "env" not in server
|
||||
assert not (root / "core" / "hook_runner.py").exists()
|
||||
assert not (root / "core" / "flush_worker.py").exists()
|
||||
assert not (root / "agents").exists()
|
||||
for skill in (root / "skills").glob("*/SKILL.md"):
|
||||
frontmatter = skill.read_text(encoding="utf-8").split("---", 2)[1]
|
||||
keys = {line.split(":", 1)[0] for line in frontmatter.splitlines() if ":" in line}
|
||||
assert keys <= {"name", "description", "license", "compatibility", "metadata", "allowed-tools"}
|
||||
assert not any(path.is_symlink() for path in root.rglob("*"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_native_bundle_is_self_contained(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
|
||||
assert (root / "core" / "memory_core.py").is_file()
|
||||
assert (root / "skills" / "remember" / "SKILL.md").is_file()
|
||||
assert not any(path.is_symlink() for path in root.rglob("*"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_only_claude_bundles_sidekick(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
if host == "claude-code":
|
||||
agent = (root / "agents" / "sidekick.md").read_text()
|
||||
assert "model: sonnet" in agent
|
||||
assert "isolation: worktree" in agent
|
||||
else:
|
||||
assert not (root / "agents").exists()
|
||||
for path in root.rglob("*.json"):
|
||||
assert "sidekick" not in path.read_text().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_native_control_skills_select_the_host_store(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
status = (root / "skills" / "status" / "SKILL.md").read_text(encoding="utf-8")
|
||||
|
||||
assert f'--harness "{host}"' in status
|
||||
if host == "claude-code":
|
||||
assert '--plugin-data-dir "${CLAUDE_PLUGIN_DATA}"' in status
|
||||
elif host == "codex":
|
||||
assert '--plugin-data-dir "${PLUGIN_DATA}"' in status
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("host", "kind"),
|
||||
[
|
||||
("mem0-agent-plugin", "portable"),
|
||||
("claude-code", "native"),
|
||||
("cursor", "native"),
|
||||
("codex", "native"),
|
||||
("kimi", "native"),
|
||||
("antigravity", "native"),
|
||||
],
|
||||
)
|
||||
def test_installable_plugin_directories_are_current(host: str, kind: str) -> None:
|
||||
assert bundle_drift(host, kind) == []
|
||||
|
||||
|
||||
def test_marketplaces_keep_public_names_and_reference_real_plugins() -> None:
|
||||
marketplace = json.loads((REPOSITORY_ROOT / "marketplace.json").read_text(encoding="utf-8"))
|
||||
sources = {plugin["name"]: plugin["source"] for plugin in marketplace["plugins"]}
|
||||
|
||||
assert sources == {"mem0": "./integrations/claude-code-plugin"}
|
||||
for source in sources.values():
|
||||
assert (REPOSITORY_ROOT / source).exists()
|
||||
|
||||
codex_marketplace = json.loads(
|
||||
(REPOSITORY_ROOT / ".agents" / "plugins" / "marketplace.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert [plugin["name"] for plugin in codex_marketplace["plugins"]] == ["mem0"]
|
||||
codex = codex_marketplace["plugins"][0]
|
||||
assert codex["source"]["path"] == "./integrations/codex-plugin"
|
||||
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from conformance import run as conformance_run # noqa: E402
|
||||
from conformance.run import _command_check # noqa: E402
|
||||
|
||||
|
||||
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNNER = PLUGIN_ROOT / "conformance" / "run.py"
|
||||
PYTHON_HOSTS = {"claude-code", "cursor", "codex", "kimi", "antigravity"}
|
||||
|
||||
|
||||
def test_python_bundle_conformance_builds_every_host(tmp_path: Path) -> None:
|
||||
report = tmp_path / "report.json"
|
||||
artifacts = tmp_path / "artifacts"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(RUNNER),
|
||||
"--group",
|
||||
"python-bundles",
|
||||
"--artifacts-dir",
|
||||
str(artifacts),
|
||||
"--report",
|
||||
str(report),
|
||||
],
|
||||
cwd=PLUGIN_ROOT.parents[1],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
payload = json.loads(report.read_text(encoding="utf-8"))
|
||||
assert payload["status"] == "passed"
|
||||
assert {
|
||||
entry["host"]
|
||||
for entry in payload["checks"]
|
||||
if entry["kind"] == "native"
|
||||
} == PYTHON_HOSTS
|
||||
assert {
|
||||
entry["host"]
|
||||
for entry in payload["checks"]
|
||||
if entry["kind"] == "portable"
|
||||
} == {"mem0-agent-plugin"}
|
||||
for host in PYTHON_HOSTS:
|
||||
assert (artifacts / host).is_dir()
|
||||
assert (artifacts / "mem0-agent-plugin").is_dir()
|
||||
|
||||
|
||||
def test_conformance_plan_covers_every_runtime(tmp_path: Path) -> None:
|
||||
report = tmp_path / "plan.json"
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(RUNNER), "--list", "--report", str(report)],
|
||||
cwd=PLUGIN_ROOT.parents[1],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
payload = json.loads(report.read_text(encoding="utf-8"))
|
||||
assert {entry["group"] for entry in payload["checks"]} == {
|
||||
"python-bundles",
|
||||
"python-tests",
|
||||
"typescript-core",
|
||||
"openclaw",
|
||||
"opencode",
|
||||
"pi-agent",
|
||||
"deepseek",
|
||||
}
|
||||
assert all(entry["status"] == "planned" for entry in payload["checks"])
|
||||
assert {
|
||||
entry["group"]
|
||||
for entry in payload["checks"]
|
||||
if entry["name"].endswith("-artifact")
|
||||
} == {"openclaw", "opencode", "pi-agent", "deepseek"}
|
||||
|
||||
|
||||
def test_typescript_artifact_check_rejects_monorepo_imports(tmp_path: Path) -> None:
|
||||
dist = tmp_path / "dist"
|
||||
dist.mkdir()
|
||||
(dist / "index.js").write_text(
|
||||
'import { createMemoryLifecycle } from "../../agent-plugin-core/typescript/src/lifecycle.ts";\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
artifact_check = getattr(conformance_run, "_typescript_artifact_check", None)
|
||||
assert artifact_check is not None, "TypeScript package artifacts are not checked"
|
||||
result = artifact_check("example", tmp_path, ("dist/index.js",))
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert "monorepo source import" in result["output"]
|
||||
|
||||
|
||||
def test_live_conformance_requires_an_explicit_mem0_key(tmp_path: Path) -> None:
|
||||
environment = dict(os.environ)
|
||||
environment.pop("MEM0_API_KEY", None)
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(RUNNER), "--live", "--report", str(tmp_path / "report.json")],
|
||||
cwd=PLUGIN_ROOT.parents[1],
|
||||
env=environment,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "MEM0_API_KEY is required for --live" in result.stderr
|
||||
|
||||
|
||||
def test_live_conformance_reports_platform_failure_without_crashing(tmp_path: Path) -> None:
|
||||
report = tmp_path / "report.json"
|
||||
environment = {
|
||||
**os.environ,
|
||||
"MEM0_API_KEY": "m0-intentionally-invalid",
|
||||
"MEM0_API_URL": "http://127.0.0.1:1",
|
||||
}
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(RUNNER),
|
||||
"--group",
|
||||
"live-platform",
|
||||
"--live",
|
||||
"--report",
|
||||
str(report),
|
||||
],
|
||||
cwd=PLUGIN_ROOT.parents[1],
|
||||
env=environment,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
payload = json.loads(report.read_text(encoding="utf-8"))
|
||||
assert payload["status"] == "failed"
|
||||
assert payload["checks"][0]["group"] == "live-platform"
|
||||
|
||||
|
||||
def test_runtime_checks_preserve_the_callers_telemetry_setting(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("MEM0_TELEMETRY", raising=False)
|
||||
|
||||
result = _command_check(
|
||||
"environment",
|
||||
"test",
|
||||
[sys.executable, "-c", "import os; print(os.environ.get('MEM0_TELEMETRY', 'unset'))"],
|
||||
cwd=tmp_path,
|
||||
)
|
||||
|
||||
assert result["status"] == "passed"
|
||||
assert result["output"] == "unset"
|
||||
|
||||
|
||||
def test_command_check_can_force_non_interactive_installs(tmp_path: Path) -> None:
|
||||
result = _command_check(
|
||||
"environment",
|
||||
"test",
|
||||
[sys.executable, "-c", "import os; print(os.environ['CI'])"],
|
||||
cwd=tmp_path,
|
||||
environment_overrides={"CI": "true"},
|
||||
)
|
||||
|
||||
assert result["status"] == "passed"
|
||||
assert result["output"] == "true"
|
||||
|
||||
|
||||
def test_conformance_report_redacts_command_output(tmp_path: Path) -> None:
|
||||
secret = "sk-eval-12345678901234567890"
|
||||
|
||||
result = _command_check(
|
||||
"redaction",
|
||||
"test",
|
||||
[sys.executable, "-c", f"print('failure: {secret}')"],
|
||||
cwd=tmp_path,
|
||||
)
|
||||
|
||||
assert secret not in json.dumps(result)
|
||||
assert "[REDACTED]" in result["output"]
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Exercise bundled subagent hooks as separate host processes, without Mem0 calls."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "python"))
|
||||
|
||||
from memory_core import EvidenceStore, RepoContext, checkpoint_stats # noqa: E402
|
||||
|
||||
|
||||
def test_generic_tracking_preserves_legacy_runs_and_can_finish_them(tmp_path):
|
||||
database = tmp_path / "evidence.sqlite3"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.executescript("""
|
||||
CREATE TABLE sidekick_runs (
|
||||
repo_id TEXT NOT NULL, session_id TEXT NOT NULL,
|
||||
agent_id TEXT NOT NULL, agent_type TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL, stopped_at TEXT, transcript_path TEXT,
|
||||
context_chars INTEGER NOT NULL DEFAULT 0, final_message TEXT,
|
||||
PRIMARY KEY(repo_id, session_id, agent_id)
|
||||
);
|
||||
INSERT INTO sidekick_runs VALUES
|
||||
('repo', 'session', 'worker', 'default', '2026-09-10', NULL, NULL, 123, NULL);
|
||||
""")
|
||||
for _ in range(2):
|
||||
store = EvidenceStore(database)
|
||||
try:
|
||||
assert store.status("repo")["subagent_runs"] == 1
|
||||
row = store.conn.execute("SELECT * FROM sidekick_runs").fetchone()
|
||||
assert row["agent_id"] == "worker"
|
||||
assert row["context_chars"] == 123
|
||||
repo = RepoContext(str(tmp_path), str(tmp_path), "repo", "app", "main", "")
|
||||
store.stop_subagent(repo, "session", "worker", "default", "", "Completed after upgrade.")
|
||||
assert store.status("repo")["last_subagent"]["stopped_at"]
|
||||
assert store.conn.execute("SELECT final_message FROM sidekick_runs").fetchone()[0] == (
|
||||
"Completed after upgrade."
|
||||
)
|
||||
finally:
|
||||
store.close()
|
||||
assert checkpoint_stats([{"kind": "sidekick_stop", "payload": {"final_message": "Legacy result."}}]) == (0, 1, 14)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "codex"])
|
||||
def test_subagent_hooks_preserve_parent_scope_and_correlate_completion(tmp_path, host):
|
||||
parent = tmp_path / "parent"
|
||||
child = tmp_path / "child-worktree"
|
||||
parent.mkdir()
|
||||
child.mkdir()
|
||||
database = tmp_path / "data" / "evidence.sqlite3"
|
||||
store = EvidenceStore(database)
|
||||
repo = store.repo_for_session("parent-session", str(parent))
|
||||
store.mark_injected("parent-session", repo.identity, [{"id": "one", "memory": "Parent memory marker."}])
|
||||
store.mark_injected("other-session", repo.identity, [{"id": "two", "memory": "Foreign memory marker."}])
|
||||
store.close()
|
||||
|
||||
plugin = ROOT.parent / f"{host}-plugin"
|
||||
adapter = plugin / ("adapters/claude/hook.py" if host == "claude-code" else "hooks/adapter.py")
|
||||
start, stop = "sidekick-start", "sidekick-stop"
|
||||
payload = {"session_id": "parent-session", "cwd": str(child), "agent_id": "worker", "agent_type": "default"}
|
||||
response_key = "last_assistant_message"
|
||||
if host == "codex":
|
||||
start, stop = "subagent-start", "subagent-stop"
|
||||
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith(("MEM0_", "CLAUDE_PLUGIN_"))}
|
||||
env.update(MEM0_CODE_DATA_DIR=str(database.parent), MEM0_TELEMETRY="false", MEM0_API_URL="http://127.0.0.1:1")
|
||||
|
||||
def invoke(event, body):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(adapter), event],
|
||||
input=json.dumps({**body, "hook_event_name": event}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
timeout=15,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
output = invoke(start, payload)
|
||||
context = json.loads(output)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "Parent memory marker." in context
|
||||
assert "Foreign memory marker." not in output
|
||||
assert "Parent memory marker." not in invoke(start, payload)
|
||||
invoke(stop, {**payload, response_key: "Finished the delegated task."})
|
||||
|
||||
store = EvidenceStore(database)
|
||||
runs = store.conn.execute("SELECT * FROM sidekick_runs").fetchall()
|
||||
store.close()
|
||||
assert len(runs) == 1
|
||||
assert (runs[0]["repo_id"], runs[0]["session_id"], runs[0]["agent_id"]) == (
|
||||
repo.identity, "parent-session", "worker"
|
||||
)
|
||||
assert runs[0]["stopped_at"]
|
||||
assert runs[0]["final_message"] == "Finished the delegated task."
|
||||
assert runs[0]["context_chars"] > 0
|
||||
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VALIDATE = ROOT / "build" / "validate.py"
|
||||
PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
|
||||
|
||||
|
||||
def run_validator(bundle: Path) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(VALIDATE), str(bundle), "--kind", "portable"],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def write_manifest(bundle: Path, schema: str = PLUGIN_SCHEMA) -> None:
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "plugin.json").write_text(
|
||||
json.dumps({"$schema": schema, "name": "mem0"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_wrong_agent_plugin_schema(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "plugin"
|
||||
write_manifest(bundle, "https://agent-plugins.org/v1.0.0/plugin.schema.json")
|
||||
|
||||
result = run_validator(bundle)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert result.stderr == (
|
||||
"plugin.json.$schema: "
|
||||
"'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json' was expected\n"
|
||||
)
|
||||
|
||||
|
||||
def test_rejects_symlink_outside_bundle(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "plugin"
|
||||
write_manifest(bundle)
|
||||
outside = tmp_path / "outside"
|
||||
outside.mkdir()
|
||||
(bundle / "skills").symlink_to(outside, target_is_directory=True)
|
||||
|
||||
result = run_validator(bundle)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert result.stderr == "skills: symlinks are not allowed in release bundles\n"
|
||||
|
||||
|
||||
def test_accepts_minimal_portable_bundle(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "plugin"
|
||||
write_manifest(bundle)
|
||||
|
||||
result = run_validator(bundle)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert result.stdout == f"Validated portable bundle: {bundle}\n"
|
||||
assert result.stderr == ""
|
||||
|
||||
|
||||
def test_rejects_nonconformant_agent_skill(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "plugin"
|
||||
write_manifest(bundle)
|
||||
skill = bundle / "skills" / "Bad_Name"
|
||||
skill.mkdir(parents=True)
|
||||
(skill / "SKILL.md").write_text(
|
||||
"---\nname: Bad_Name\ndescription: invalid\nunknown: true\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = run_validator(bundle)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "skills/Bad_Name" in result.stderr
|
||||
|
||||
|
||||
def run_native_validator(bundle: Path) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(VALIDATE), str(bundle), "--kind", "native"],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def test_native_bundle_rejects_malformed_json(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "plugin"
|
||||
bundle.mkdir(parents=True)
|
||||
(bundle / "plugin.json").write_text("{bad json", encoding="utf-8")
|
||||
|
||||
result = run_native_validator(bundle)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "plugin.json" in result.stderr
|
||||
|
||||
|
||||
def test_native_bundle_accepts_valid_json(tmp_path: Path) -> None:
|
||||
bundle = tmp_path / "plugin"
|
||||
bundle.mkdir(parents=True)
|
||||
(bundle / "plugin.json").write_text('{"name": "mem0"}', encoding="utf-8")
|
||||
hooks = bundle / "hooks"
|
||||
hooks.mkdir()
|
||||
(hooks / "hooks.json").write_text('{"version": 1}', encoding="utf-8")
|
||||
|
||||
result = run_native_validator(bundle)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "native" in result.stdout
|
||||
Reference in New Issue
Block a user