chore: import upstream snapshot with attribution
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""Tests for the instance-level store APIs: upsert, remove, filter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from integrations.store import (
|
||||
get_instance,
|
||||
get_instances,
|
||||
remove_instance,
|
||||
upsert_instance,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_store(tmp_path: Path):
|
||||
store_file = tmp_path / "integrations.json"
|
||||
with patch("integrations.store.STORE_PATH", store_file):
|
||||
yield store_file
|
||||
|
||||
|
||||
def _seed(store_file: Path, records: list[dict]) -> None:
|
||||
store_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
store_file.write_text(json.dumps({"version": 2, "integrations": records}) + "\n")
|
||||
|
||||
|
||||
def test_upsert_instance_appends_to_existing_record(tmp_store: Path) -> None:
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"service": "grafana",
|
||||
"status": "active",
|
||||
"instances": [{"name": "prod", "tags": {}, "credentials": {"endpoint": "a"}}],
|
||||
}
|
||||
],
|
||||
)
|
||||
upsert_instance(
|
||||
"grafana",
|
||||
{"name": "staging", "tags": {"env": "staging"}, "credentials": {"endpoint": "b"}},
|
||||
record_id="g1",
|
||||
)
|
||||
instances = get_instances("grafana")
|
||||
names = {i["name"] for i in instances}
|
||||
assert names == {"prod", "staging"}
|
||||
|
||||
|
||||
def test_upsert_instance_updates_by_name_not_position(tmp_store: Path) -> None:
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"service": "grafana",
|
||||
"status": "active",
|
||||
"instances": [
|
||||
{"name": "prod", "tags": {}, "credentials": {"endpoint": "old"}},
|
||||
{"name": "staging", "tags": {}, "credentials": {"endpoint": "s"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
upsert_instance(
|
||||
"grafana",
|
||||
{"name": "prod", "tags": {}, "credentials": {"endpoint": "new"}},
|
||||
record_id="g1",
|
||||
)
|
||||
prod = get_instance("grafana", name="prod")
|
||||
assert prod is not None
|
||||
assert prod["credentials"]["endpoint"] == "new"
|
||||
# staging is still there
|
||||
staging = get_instance("grafana", name="staging")
|
||||
assert staging is not None
|
||||
assert staging["credentials"]["endpoint"] == "s"
|
||||
|
||||
|
||||
def test_upsert_instance_creates_new_record_when_no_record_id(tmp_store: Path) -> None:
|
||||
upsert_instance(
|
||||
"grafana",
|
||||
{"name": "prod", "tags": {}, "credentials": {"endpoint": "p"}},
|
||||
)
|
||||
instances = get_instances("grafana")
|
||||
assert len(instances) == 1
|
||||
assert instances[0]["name"] == "prod"
|
||||
|
||||
|
||||
def test_get_instance_by_name_returns_only_that_instance(tmp_store: Path) -> None:
|
||||
"""PR #527 bug #3 regression: filtered get must not leak sibling instances."""
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"service": "grafana",
|
||||
"status": "active",
|
||||
"instances": [
|
||||
{"name": "prod", "tags": {}, "credentials": {"endpoint": "p"}},
|
||||
{"name": "staging", "tags": {}, "credentials": {"endpoint": "s"}},
|
||||
{"name": "dev", "tags": {}, "credentials": {"endpoint": "d"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
prod = get_instance("grafana", name="prod")
|
||||
assert prod is not None
|
||||
assert prod["name"] == "prod"
|
||||
assert prod["credentials"]["endpoint"] == "p"
|
||||
# Result must carry ONLY the prod instance — not a record with siblings
|
||||
assert "instances" not in prod # shape is the instance dict, not the parent record
|
||||
assert "staging" not in json.dumps(prod)
|
||||
assert "dev" not in json.dumps(prod)
|
||||
|
||||
|
||||
def test_get_instance_by_tag(tmp_store: Path) -> None:
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"service": "grafana",
|
||||
"status": "active",
|
||||
"instances": [
|
||||
{"name": "prod", "tags": {"env": "prod"}, "credentials": {"endpoint": "p"}},
|
||||
{
|
||||
"name": "staging",
|
||||
"tags": {"env": "staging"},
|
||||
"credentials": {"endpoint": "s"},
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
match = get_instance("grafana", tags={"env": "staging"})
|
||||
assert match is not None
|
||||
assert match["name"] == "staging"
|
||||
|
||||
|
||||
def test_remove_instance_persists_when_record_retains_others(tmp_store: Path) -> None:
|
||||
"""PR #527 P2 regression: removing one instance must save to disk even
|
||||
when the parent record is retained."""
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"service": "grafana",
|
||||
"status": "active",
|
||||
"instances": [
|
||||
{"name": "prod", "tags": {}, "credentials": {"endpoint": "p"}},
|
||||
{"name": "staging", "tags": {}, "credentials": {"endpoint": "s"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
removed = remove_instance("grafana", "prod")
|
||||
assert removed is True
|
||||
|
||||
# Reload from disk to confirm persistence.
|
||||
data = json.loads(tmp_store.read_text())
|
||||
remaining_names = [i["name"] for i in data["integrations"][0]["instances"]]
|
||||
assert remaining_names == ["staging"]
|
||||
|
||||
|
||||
def test_remove_last_instance_removes_record(tmp_store: Path) -> None:
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "g1",
|
||||
"service": "grafana",
|
||||
"status": "active",
|
||||
"instances": [
|
||||
{"name": "prod", "tags": {}, "credentials": {"endpoint": "p"}},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
removed = remove_instance("grafana", "prod")
|
||||
assert removed is True
|
||||
|
||||
data = json.loads(tmp_store.read_text())
|
||||
assert data["integrations"] == []
|
||||
|
||||
|
||||
def test_remove_instance_returns_false_when_not_found(tmp_store: Path) -> None:
|
||||
_seed(tmp_store, [])
|
||||
assert remove_instance("grafana", "prod") is False
|
||||
|
||||
|
||||
def test_get_integration_provides_flat_credentials_view(tmp_store: Path) -> None:
|
||||
"""Backward compat: callers like azure_sql.py / mysql.py / postgresql.py
|
||||
read ``record['credentials']`` directly. get_integration must synthesise
|
||||
that view from ``instances[0].credentials`` so they keep working."""
|
||||
from integrations.store import get_integration
|
||||
|
||||
_seed(
|
||||
tmp_store,
|
||||
[
|
||||
{
|
||||
"id": "mysql-1",
|
||||
"service": "mysql",
|
||||
"status": "active",
|
||||
"instances": [
|
||||
{
|
||||
"name": "default",
|
||||
"tags": {},
|
||||
"credentials": {"host": "db.example.com", "database": "prod"},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
record = get_integration("mysql")
|
||||
assert record is not None
|
||||
# v2 shape is still present
|
||||
assert "instances" in record
|
||||
# AND the flat credentials view for legacy callers
|
||||
assert record["credentials"]["host"] == "db.example.com"
|
||||
assert record["credentials"]["database"] == "prod"
|
||||
Reference in New Issue
Block a user