Initial project import

This commit is contained in:
2026-08-14 09:40:51 +03:00
parent 00040e5ce4
commit d7099bf80d
146 changed files with 30509 additions and 1055 deletions
+36
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
MODULE = Path(__file__).parents[2] / "plugins" / "1c" / "observer" / "observer_server.py"
SPEC = importlib.util.spec_from_file_location("adapter_observer", MODULE)
assert SPEC and SPEC.loader
observer = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(observer)
def test_summary_separates_expected_rejection_and_exception() -> None:
events = [
{"event": "adapter_rpc", "time": "2026-08-07T10:00:00Z", "request_id": "a", "method": "metadata.form.decode", "request": {"base_id": "upo_test", "ref": "Document.X"}, "status": "ok", "duration_ms": 100},
{"event": "adapter_rpc", "time": "2026-08-07T10:01:00Z", "request_id": "b", "method": "metadata.form.decode", "request": {"base_id": "upo_test"}, "status": "partial", "error": "time_budget_exhausted", "duration_ms": 6000},
{"event": "adapter_rpc", "time": "2026-08-07T10:02:00Z", "request_id": "c", "method": "modules.read", "request": {"base_id": "upo_test"}, "status": "exception", "error": "request_exception", "duration_ms": 10},
{"event": "adapter_rpc", "time": "2026-08-07T10:03:00Z", "request_id": "d", "method": "adapter.job.get", "request": {"base_id": "upo_test"}, "status": "running", "duration_ms": 1},
]
result = observer.build_summary(events, 0)
assert result["events"] == 4
assert result["exceptions"] == 1
assert result["p95_ms"] == 6000
assert any(item["error"] == "time_budget_exhausted" for item in result["findings"])
assert not any(item["status"] == "running" for item in result["findings"])
assert "BSL" not in json.dumps(observer.event_view(events[0]), ensure_ascii=False)
def test_correlations_keep_mcp_request_that_did_not_reach_rest() -> None:
rest = [{"request_id": "same", "method": "modules.read", "status": "ok", "duration_ms": 20}]
mcp = [{"request_id": "same", "method": "modules.read", "status": "ok", "duration_ms": 30}, {"request_id": "lost", "method": "modules.read", "status": "exception", "error": "adapter_unavailable"}]
result = observer.correlations(rest, mcp)
assert result[0]["correlation_status"] == "not_reached_rest"
assert result[1]["correlation_status"] == "matched"
+138
View File
@@ -0,0 +1,138 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
CONNECTOR = ROOT / "plugins" / "1c" / "connector"
if str(CONNECTOR) not in sys.path:
sys.path.insert(0, str(CONNECTOR))
import adapter_1c_server as adapter # noqa: E402
def test_additional_attributes_find_reports_empty_source_not_silent_empty(monkeypatch) -> None:
monkeypatch.setattr(adapter, "require_base_id", lambda _payload, _method: "upo_test")
monkeypatch.setattr(
adapter,
"data_read",
lambda _payload, method: {
"status": "ok",
"object": {"ref": "ChartOfCharacteristicTypes.ДополнительныеРеквизитыИСведения"},
"rows": [],
"counts": {"rows": 0},
"route_diagnostics": {"status": "empty_source", "source_count": 0},
},
)
result = adapter.additional_attributes_find({"base_id": "upo_test", "query": "Ответственное направление"})
assert result["status"] == "empty_source"
assert result["properties"] == []
assert result["diagnostics"]["route"]["source_count"] == 0
def test_additional_attributes_storage_refuses_to_guess_storage_route() -> None:
result = adapter.additional_attributes_storage_resolve(
{
"base_id": "upo_test",
"property_ref": "0123456789ABCDEF0123456789ABCDEF",
"owner_ref": "Catalog.СтруктурныеЕдиницы",
}
)
assert result["status"] == "unresolved"
assert result["storage"] is None
assert "will not infer" in result["diagnostics"]["message"]
def test_additional_attributes_storage_confirms_metadata_evidenced_route(monkeypatch) -> None:
monkeypatch.setattr(adapter, "list_objects", lambda *_args, **_kwargs: {"objects": [{"ref": "InformationRegister.ДополнительныеСведения"}]})
monkeypatch.setattr(
adapter,
"metadata_object_attributes",
lambda _payload: {
"status": "ok",
"dimensions": [
{"name": "Объект", "type": {"kind": "reference"}},
{"name": "Свойство", "type": {"resolved": {"kind": "ChartOfCharacteristicTypes", "name": "ДополнительныеРеквизитыИСведения"}}},
],
"resources": [{"name": "Значение", "type": {"kind": "composite"}}],
},
)
result = adapter.additional_attributes_storage_resolve(
{
"base_id": "upo_test",
"property_ref": "0123456789ABCDEF0123456789ABCDEF",
"owner_ref": "Catalog.СтруктурныеЕдиницы",
}
)
assert result["status"] == "confirmed"
assert result["storage"]["source_ref"] == "InformationRegister.ДополнительныеСведения"
assert result["storage"]["fields"] == {"object": "Объект", "property": "Свойство", "value": "Значение"}
assert result["scd_join"]["parameters"]["Свойство"] == "0123456789ABCDEF0123456789ABCDEF"
def test_additional_attributes_storage_requires_public_inputs() -> None:
result = adapter.additional_attributes_storage_resolve({"base_id": "upo_test", "owner_ref": "Catalog.СтруктурныеЕдиницы"})
assert result["status"] == "invalid_argument"
assert result["argument"] == "property_ref"
def test_chart_semantics_exposes_only_evidenced_chart_level_value_types(monkeypatch) -> None:
monkeypatch.setattr(adapter, "config_tree_item_at_path", lambda _tree, path: {"path": path})
monkeypatch.setattr(adapter, "public_pattern_value_type", lambda *_args, **_kwargs: {"kind": "union", "count": 2})
result = adapter.chart_of_characteristic_types_sql_details("upo_test", {"type": "list"}, table="Config", timeout_seconds=30)
assert result["allowed_value_types"] == {"kind": "union", "count": 2}
assert result["evidence"]["config_path"] == "1.18"
def test_chart_data_list_accepts_description_contains_filter(monkeypatch) -> None:
monkeypatch.setattr(
adapter,
"data_object_schema",
lambda _payload: {
"status": "ok",
"base_id": "upo_test",
"table": {"name": "_Chrc1"},
"object": {"kind": "ChartOfCharacteristicTypes"},
"fields": [
{"name": "ref", "physical_name": "_IDRRef", "type": {}},
{"name": "description", "physical_name": "_Description", "type": {}},
{"name": "marked_for_deletion", "physical_name": "_Marked", "type": {}},
],
},
)
captured: dict[str, object] = {}
def fake_sql(_base_id, sql, params, **_kwargs):
captured["sql"] = sql
captured["params"] = params
return [], None
monkeypatch.setattr(adapter, "data_sql_rows", fake_sql)
result = adapter.data_read(
{
"base_id": "upo_test",
"ref": "ChartOfCharacteristicTypes.Тест",
"fields": ["ref", "description"],
"filters": {"description": {"operator": "contains", "value": "Ответственное направление"}},
}
)
assert result["status"] == "ok"
assert "[_Description] LIKE %s" in str(captured["sql"])
assert "%Ответственное направление%" in captured["params"]
def test_password_test_mode_accepts_explicit_global_unauthenticated_profile(monkeypatch) -> None:
monkeypatch.delenv("ONEC_INFOBASE_USER_ADMIN_ALLOW_UNAUTHENTICATED", raising=False)
monkeypatch.setenv("ONEC_ADAPTER_ALLOW_UNAUTHENTICATED_ADMIN", "true")
assert adapter.infobase_user_password_unauthenticated_test_mode() is True
@@ -0,0 +1,105 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
import embed_1c_code_vectors as worker # noqa: E402
def test_embed_pending_code_vectors_upserts_with_precondition(monkeypatch) -> None:
calls: list[tuple[str, dict]] = []
def fake_adapter_call(
adapter_url: str,
method: str,
payload: dict,
*,
timeout_seconds: int = 180,
) -> dict:
calls.append((method, payload))
if method == "metadata.code_vector.pending":
assert payload["embedding_model"] == "local-hashing-v1"
assert payload["chunk_kinds"] == ["routine"]
assert payload["max_text_chars"] == 4000
return {
"status": "ok",
"chunks": [
{
"chunk_id": "chunk-1",
"text_sha1": "a" * 40,
"text": "Процедура РассчитатьНалог()",
}
],
}
if method == "metadata.code_vector.embedding.upsert":
assert payload["chunk_id"] == "chunk-1"
assert payload["text_sha1"] == "a" * 40
assert payload["embedding_model"] == "local-hashing-v1"
assert len(payload["embedding"]) == 16
return {"status": "ok", "dimensions": 16}
raise AssertionError(method)
monkeypatch.setattr(worker, "adapter_call", fake_adapter_call)
result = worker.embed_pending_code_vectors(
adapter_url="http://adapter/rpc",
base_id="upo_test",
limit=1,
dimensions=16,
)
assert result["status"] == "ok"
assert result["embedding"]["dimensions"] == 16
assert result["counts"] == {
"pending": 1,
"processed": 1,
"stored": 1,
"conflicts": 0,
"skipped": 0,
"errors": 0,
}
assert [method for method, _payload in calls] == [
"metadata.code_vector.pending",
"metadata.code_vector.embedding.upsert",
]
def test_embed_pending_code_vectors_dry_run_does_not_upsert(monkeypatch) -> None:
calls: list[str] = []
def fake_adapter_call(
adapter_url: str,
method: str,
payload: dict,
*,
timeout_seconds: int = 180,
) -> dict:
calls.append(method)
return {
"status": "ok",
"chunks": [
{
"chunk_id": "chunk-1",
"text_sha1": "b" * 40,
"text": "Функция НайтиОбъект()",
}
],
}
monkeypatch.setattr(worker, "adapter_call", fake_adapter_call)
result = worker.embed_pending_code_vectors(
adapter_url="http://adapter/rpc",
base_id="upo_test",
dimensions=8,
dry_run=True,
)
assert result["counts"]["processed"] == 1
assert result["counts"]["stored"] == 0
assert result["upserts"][0]["status"] == "dry_run"
assert calls == ["metadata.code_vector.pending"]
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "scripts"))
import search_1c_code_vectors as search_cli # noqa: E402
def test_search_code_vectors_sends_model_and_strict_query_embedding(monkeypatch) -> None:
calls: list[tuple[str, dict]] = []
monkeypatch.setattr(
search_cli,
"embed_texts",
lambda texts, **kwargs: [[1.0, 0.0, 0.0]],
)
def fake_adapter_call(
adapter_url: str,
method: str,
payload: dict,
*,
timeout_seconds: int = 180,
) -> dict:
calls.append((method, payload))
return {"status": "ok", "matches": []}
monkeypatch.setattr(search_cli, "adapter_call", fake_adapter_call)
result = search_cli.search_code_vectors(
adapter_url="http://adapter/rpc",
base_id="upo_test",
query="где рассчитывается налог",
embedding_provider="openai-compatible",
embedding_model="test-code-model",
embedding_base_url="http://embeddings",
dimensions=3,
)
assert result["status"] == "ok"
assert result["client_embedding"]["stored_embedding_model"] == "openai-compatible:test-code-model@d3"
assert calls == [
(
"metadata.code_vector.search",
{
"base_id": "upo_test",
"query": "где рассчитывается налог",
"query_embedding": [1.0, 0.0, 0.0],
"embedding_model": "openai-compatible:test-code-model@d3",
"limit": 10,
"scan_limit": 2000,
"verify": True,
"strict": True,
},
)
]
def test_qwen3_search_uses_query_instruction_but_keeps_lexical_query_plain(monkeypatch) -> None:
embedded_texts: list[str] = []
calls: list[tuple[str, dict]] = []
def fake_embed_texts(texts: list[str], **kwargs) -> list[list[float]]:
embedded_texts.extend(texts)
return [[0.5, 0.5]]
monkeypatch.setattr(search_cli, "embed_texts", fake_embed_texts)
def fake_adapter_call(
adapter_url: str,
method: str,
payload: dict,
*,
timeout_seconds: int = 180,
) -> dict:
calls.append((method, payload))
return {"status": "ok", "matches": []}
monkeypatch.setattr(search_cli, "adapter_call", fake_adapter_call)
result = search_cli.search_code_vectors(
adapter_url="http://adapter/rpc",
base_id="upo_test",
query="обработка проведения документа",
embedding_provider="openai-compatible",
embedding_model="qwen3-embedding-0.6b",
embedding_base_url="http://embeddings",
)
assert embedded_texts == [
"Instruct: Given a natural-language software task, retrieve the relevant "
"1C Enterprise BSL source-code fragment that implements or explains it\n"
"Query:обработка проведения документа"
]
assert calls[0][1]["query"] == "обработка проведения документа"
assert result["client_embedding"]["query_instruction"] == (
search_cli.DEFAULT_QWEN3_CODE_RETRIEVAL_INSTRUCTION
)
@@ -0,0 +1,40 @@
"""Regression checks for SQL saved-state → Configurator refresh guidance."""
from __future__ import annotations
import sys
from pathlib import Path
CONNECTOR = Path(__file__).resolve().parents[2] / "plugins" / "1c" / "connector"
if str(CONNECTOR) not in sys.path:
sys.path.insert(0, str(CONNECTOR))
import adapter_1c_server as adapter # noqa: E402
EXTENSION_FILE = "fb26cf42-7609-11f1-828f-005056b0d483__77494708-43ea-4956-ac3c-199cfb035ad2.0"
def test_existing_extension_saved_object_reopens_object() -> None:
result = adapter.saved_state_configurator_refresh_guidance(
table="ConfigCASSave", file_name=EXTENSION_FILE, object_existed_in_saved_state=True
)
assert result["scope"] == "object"
assert result["action"] == "close_reopen_object"
def test_new_extension_saved_object_reopens_extension() -> None:
result = adapter.saved_state_configurator_refresh_guidance(
table="ConfigCASSave", file_name=EXTENSION_FILE, object_existed_in_saved_state=False
)
assert result["scope"] == "extension"
assert result["action"] == "close_reopen_extension"
def test_new_base_saved_object_reopens_configuration() -> None:
result = adapter.saved_state_configurator_refresh_guidance(
table="ConfigSave", file_name="77494708-43ea-4956-ac3c-199cfb035ad2.0", object_existed_in_saved_state=False
)
assert result["scope"] == "configuration"
assert result["action"] == "close_reopen_configuration"
@@ -0,0 +1,62 @@
"""Extension CommonForm decoding must use the manifest payload route, not its GUID as a file name."""
from __future__ import annotations
import sys
from pathlib import Path
CONNECTOR = Path(__file__).resolve().parents[2] / "plugins" / "1c" / "connector"
if str(CONNECTOR) not in sys.path:
sys.path.insert(0, str(CONNECTOR))
import adapter_1c_server as adapter # noqa: E402
def test_common_form_uses_extension_manifest_route(monkeypatch) -> None:
form_guid = "e7517ee6-7e33-40ba-9442-48c78e0818e1"
captured: dict[str, str] = {}
monkeypatch.setattr(
adapter,
"extension_objects_find",
lambda _payload: {
"status": "ok",
"objects": [{
"kind": "CommonForm",
"name": "ФормаОтчетаПродкорзина",
"guid": form_guid,
"route": {"table": "ConfigCAS", "file_name": "manifest-hash-key"},
}],
},
)
def fake_read(_base_id, table, file_name, **_kwargs):
captured["table"] = table
captured["file_name"] = file_name
return None, None, {"status": "source_missing", "diagnostics": {"message": "fixture stop"}}
monkeypatch.setattr(adapter, "read_storage_file_bytes", fake_read)
result = adapter.metadata_form_decode(
{
"base_id": "upo_test",
"kind": "CommonForm",
"extension": "фс_Отчеты1",
"name": "ФормаОтчетаПродкорзина",
"guid": form_guid,
"source_state": "active",
}
)
assert result["status"] == "source_missing"
assert captured == {"table": "ConfigCAS", "file_name": "manifest-hash-key"}
def test_static_form_structure_marks_unproven_tree_edges() -> None:
structure = adapter.form_static_structure(
{"items": [{"id": "1", "name": "Настройки", "type_name": "Группа", "path": "1.7", "depth": 2}]}
)
assert structure["status"] == "partial"
assert structure["elements"][0]["parent"]["status"] == "unresolved"
assert structure["elements"][0]["children"]["status"] == "unresolved"
@@ -0,0 +1,193 @@
"""Pure regression fixtures for the proven ConfigInfo file-SHA1 map rewrite."""
from __future__ import annotations
import base64
import hashlib
import sys
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
CONNECTOR = ROOT / "plugins" / "1c" / "connector"
PARSER = ROOT / "plugins" / "1c"
for path in (CONNECTOR, PARSER):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
import adapter_1c_server as adapter # noqa: E402
from parser.payload import decode_payload_lossless # noqa: E402
LOGICAL = "77494708-43ea-4956-ac3c-199cfb035ad2.0"
def raw_deflate(text: str) -> bytes:
encoder = zlib.compressobj(wbits=-zlib.MAX_WBITS)
return encoder.compress(text.encode("utf-8")) + encoder.flush()
def test_rewrites_only_named_file_sha1_reference() -> None:
original_file = b"old module bytes"
replacement_file = b"new module bytes"
old_ref = base64.b64encode(hashlib.sha1(original_file).digest()).decode("ascii")
service_atom = base64.b64encode(b"S" * 32).decode("ascii")
source_text = f'{{2,extension,{service_atom}}},{{7,"{LOGICAL}",{old_ref},"other.0",{old_ref}}}'
result = adapter.rewrite_extension_configinfo_file_sha1_reference(
raw_deflate(source_text), logical_file_name=LOGICAL, replacement_file_data=replacement_file
)
assert result["status"] == "ok"
assert result["replacement_file_sha1"] == hashlib.sha1(replacement_file).hexdigest()
decoded = str(decode_payload_lossless(result["payload"]).get("text") or "")
assert service_atom in decoded
assert result["replacement_reference_base64"] in decoded
assert decoded.count(old_ref) == 1 # unrelated map entry was preserved
def test_rejects_ambiguous_logical_reference() -> None:
ref = base64.b64encode(hashlib.sha1(b"old").digest()).decode("ascii")
source_text = f'{{7,"{LOGICAL}",{ref},"{LOGICAL}",{ref}}}'
result = adapter.rewrite_extension_configinfo_file_sha1_reference(
raw_deflate(source_text), logical_file_name=LOGICAL, replacement_file_data=b"new"
)
assert result["status"] == "reference_not_unique"
assert result["occurrences"] == 2
def test_builds_paired_plan_only_when_map_matches_current_stream() -> None:
current = b"current module"
replacement = b"replacement module"
current_ref = base64.b64encode(hashlib.sha1(current).digest()).decode("ascii")
configinfo = raw_deflate(f'{{7,"{LOGICAL}",{current_ref}}}')
result = adapter.build_extension_saved_state_pair_plan(
logical_file_name=LOGICAL,
current_file_data=current,
replacement_file_data=replacement,
current_configinfo_data=configinfo,
)
assert result["status"] == "ok"
assert result["module"]["replacement_sha1"] == hashlib.sha1(replacement).hexdigest()
assert result["configinfo"]["service_atoms"] == "preserved_unmodified"
def test_rejects_pair_when_map_points_to_another_stream() -> None:
mapped = b"other module"
current = b"current module"
mapped_ref = base64.b64encode(hashlib.sha1(mapped).digest()).decode("ascii")
configinfo = raw_deflate(f'{{7,"{LOGICAL}",{mapped_ref}}}')
result = adapter.build_extension_saved_state_pair_plan(
logical_file_name=LOGICAL,
current_file_data=current,
replacement_file_data=b"replacement",
current_configinfo_data=configinfo,
)
assert result["status"] == "configinfo_reference_mismatch"
class FakeCursor:
def __init__(self, state: dict[str, bytes]) -> None:
self.state = state
self.rows: list[dict[str, object]] = []
def execute(self, sql: str, params: tuple[object, ...] | None = None) -> None:
if sql.startswith("SET LOCK_TIMEOUT"):
return
if sql.startswith("SELECT FileName, PartNo, BinaryData"):
assert params is not None
self.rows = [
{"FileName": name, "PartNo": 0, "BinaryData": self.state[name]}
for name in params
if name in self.state
]
return
if sql.startswith("UPDATE dbo.ConfigCASSave"):
assert params is not None
data, _size, name, _part = params
self.state[str(name)] = bytes(data)
return
raise AssertionError(sql)
def fetchall(self) -> list[dict[str, object]]:
return self.rows
class FakeConnection:
def __init__(self, state: dict[str, bytes]) -> None:
self.cursor_instance = FakeCursor(state)
self.committed = False
self.rolled_back = False
self.closed = False
def cursor(self, **_kwargs: object) -> FakeCursor:
return self.cursor_instance
def commit(self) -> None:
self.committed = True
def rollback(self) -> None:
self.rolled_back = True
def close(self) -> None:
self.closed = True
def install_fake_pair_sql(monkeypatch: object, state: dict[str, bytes], connection: FakeConnection) -> None:
monkeypatch.setattr(adapter, "connect_live_sql", lambda *_args, **_kwargs: (connection, {"database": "test"}, None))
monkeypatch.setattr(
adapter,
"read_storage_file_bytes",
lambda _base, _table, name, **_kwargs: (state.get(name), {"database": "test"}, None if name in state else {"status": "missing"}),
)
monkeypatch.setattr(adapter, "write_storage_apply_backup", lambda **_kwargs: {"backup_id": "test"})
monkeypatch.setattr(adapter, "invalidate_adapter_caches_after_saved_state_change", lambda *_args, **_kwargs: {"invalidated": True})
def test_atomically_updates_module_and_configinfo(monkeypatch: object) -> None:
extension = "fb26cf42-7609-11f1-828f-005056b0d483"
module_name = f"{extension}__{LOGICAL}"
configinfo_name = f"{extension}__configinfo"
current = b"current module"
current_ref = base64.b64encode(hashlib.sha1(current).digest()).decode("ascii")
state = {module_name: current, configinfo_name: raw_deflate(f'{{7,"{LOGICAL}",{current_ref}}}')}
connection = FakeConnection(state)
install_fake_pair_sql(monkeypatch, state, connection)
result = adapter.apply_extension_saved_state_pair(
"upo_test",
module_file_name=module_name,
replacement_module_data=b"new module",
expected_module_sha1=hashlib.sha1(current).hexdigest(),
proposal={"method": "test"},
)
assert result["status"] == "applied"
assert result["applied"] is True
assert connection.committed is True
assert result["configurator_refresh"]["action"] == "close_reopen_object"
map_text = str(decode_payload_lossless(state[configinfo_name]).get("text") or "")
assert base64.b64encode(hashlib.sha1(b"new module").digest()).decode("ascii") in map_text
def test_atomic_writer_rolls_back_when_configinfo_map_diverges(monkeypatch: object) -> None:
extension = "fb26cf42-7609-11f1-828f-005056b0d483"
module_name = f"{extension}__{LOGICAL}"
configinfo_name = f"{extension}__configinfo"
current = b"current module"
wrong_ref = base64.b64encode(hashlib.sha1(b"other module").digest()).decode("ascii")
state = {module_name: current, configinfo_name: raw_deflate(f'{{7,"{LOGICAL}",{wrong_ref}}}')}
connection = FakeConnection(state)
install_fake_pair_sql(monkeypatch, state, connection)
result = adapter.apply_extension_saved_state_pair(
"upo_test",
module_file_name=module_name,
replacement_module_data=b"new module",
expected_module_sha1=hashlib.sha1(current).hexdigest(),
proposal={"method": "test"},
)
assert result["status"] == "configinfo_reference_mismatch"
assert result["applied"] is False
assert connection.rolled_back is True
assert state[module_name] == current
+102
View File
@@ -0,0 +1,102 @@
"""Public MCP selectors must not leak storage coordinates to coding agents."""
from __future__ import annotations
import sys
from pathlib import Path
MCP = Path(__file__).resolve().parents[2] / "plugins" / "1c" / "mcp"
if str(MCP) not in sys.path:
sys.path.insert(0, str(MCP))
import adapter_1c_mcp as mcp # noqa: E402
def test_read_selector_is_replaced_with_opaque_token() -> None:
mcp.SELECTOR_TOKENS.clear()
result = mcp.publicize_read_selectors(
{
"read_selector": {
"method": "metadata.form.decode",
"base_id": "upo_test",
"table": "ConfigCAS",
"file_name": "hash-key",
"guid": "11111111-1111-1111-1111-111111111111",
}
}
)
selector = result["read_selector"]
assert selector["method"] == "metadata.form.decode"
assert selector["selector_token"].startswith("onecsel_")
assert "table" not in selector
assert "file_name" not in selector
payload, error = mcp.resolve_selector_token("metadata.form.decode", {"selector_token": selector["selector_token"]})
assert error is None
assert payload is not None
assert payload["table"] == "ConfigCAS"
assert payload["file_name"] == "hash-key"
assert payload["_selector_token_resolved"] is True
def test_public_result_hides_nested_technical_fields() -> None:
result = mcp.publicize_read_selectors(
{"object": {"name": "Тест", "guid": "private"}, "source": {"table": "ConfigSave"}}
)
assert result == {"object": {"name": "Тест"}, "source": {}}
def test_selector_token_cannot_be_reused_for_another_method() -> None:
token = mcp.issue_selector_token({"method": "code.read", "base_id": "upo_test", "module_ref": "private"})
payload, error = mcp.resolve_selector_token("metadata.form.decode", {"selector_token": token})
assert payload is None
assert error is not None
assert error["error"] == "selector_token_method_mismatch"
def test_normal_agent_request_rejects_technical_coordinates() -> None:
blocked = mcp.normal_agent_technical_field_guardrail(
"metadata.form.decode",
{"base_id": "upo_test", "table": "ConfigCASSave", "file_name": "private"},
)
assert blocked is not None
assert blocked["reason"] == "technical_selector_forbidden"
assert blocked["diagnostics"]["fields"] == ["file_name", "table"]
def test_normal_agent_request_rejects_nested_technical_coordinates() -> None:
blocked = mcp.normal_agent_technical_field_guardrail(
"metadata.form.decode",
{"base_id": "upo_test", "selector": {"table": "ConfigCASSave"}},
)
assert blocked is not None
assert blocked["diagnostics"]["fields"] == ["table"]
def test_diagnostic_request_may_keep_technical_coordinates(monkeypatch) -> None:
monkeypatch.setenv("ONEC_MCP_ALLOW_DIAGNOSTIC", "true")
assert mcp.normal_agent_technical_field_guardrail(
"metadata.form.decode",
{"base_id": "upo_test", "table": "ConfigCASSave", "diagnostic": True},
) is None
def test_form_methods_receive_effective_working_state() -> None:
payload = mcp.apply_freshness_request_policy(
{"base_id": "upo_test", "ref": "ОбщаяФорма.Тест"},
"metadata.form.decode",
)
assert payload["configuration_view"] == "effective_working"
assert payload["source_state"] == "working"
assert payload["state"] == "working"
def test_runtime_form_inspection_is_explicitly_unsupported() -> None:
result = mcp.runtime_form_inspection_unsupported(
"runtime.form.elements.inspect",
{"base_id": "upo_test"},
)
assert result is not None
assert result["error"] == "runtime_inspection_unsupported"
@@ -0,0 +1,58 @@
"""Component graph must expose only children confirmed by its source readers."""
from __future__ import annotations
import sys
from pathlib import Path
CONNECTOR = Path(__file__).resolve().parents[2] / "plugins" / "1c" / "connector"
if str(CONNECTOR) not in sys.path:
sys.path.insert(0, str(CONNECTOR))
import adapter_1c_server as adapter # noqa: E402
def test_components_aggregate_confirmed_children_without_inference(monkeypatch) -> None:
object_card = {"ref": "Report.Sales", "kind": "Report", "name": "Sales", "guid": "a" * 36}
monkeypatch.setattr(
adapter,
"metadata_object_modules",
lambda _: {"status": "ok", "method": "metadata.object.modules", "object": object_card,
"modules": [{"role": "object_module", "name": "Модуль объекта", "path": "Report.Sales.МодульОбъекта"}]},
)
monkeypatch.setattr(
adapter,
"metadata_object_forms",
lambda _: {"status": "ok", "method": "metadata.object.forms", "object": object_card,
"forms": [{"name": "Main", "guid": "b" * 36}]},
)
monkeypatch.setattr(
adapter,
"metadata_object_templates",
lambda _: {"status": "ok", "method": "metadata.object.templates", "object": object_card,
"templates": [{"name": "ОсновнаяСхемаКомпоновкиДанных", "guid": "c" * 36, "kind": "DataCompositionSchema"}]},
)
result = adapter.metadata_object_components(
{"base_id": "upo_test", "extension": "test2", "ref": "Report.Sales", "include_storage": False}
)
assert result["status"] == "ok"
assert [item["component_kind"] for item in result["components"]] == ["metadata_object", "module", "form", "scd"]
assert result["components"][2]["read_selector"]["method"] == "metadata.object.form.details"
assert result["components"][3]["read_selector"]["method"] == "scd.inspect"
assert result["components"][3]["read_selector"]["params"]["extension"] == "test2"
assert result["unresolved"] == []
def test_components_marks_a_failed_area_instead_of_creating_a_child(monkeypatch) -> None:
object_card = {"ref": "CommonForm.t_Форма", "kind": "CommonForm", "name": "t_Форма"}
monkeypatch.setattr(adapter, "metadata_object_modules", lambda _: {"status": "ok", "object": object_card, "modules": []})
monkeypatch.setattr(adapter, "metadata_object_forms", lambda _: {"status": "ok", "object": object_card, "forms": []})
monkeypatch.setattr(adapter, "metadata_object_templates", lambda _: {"status": "unsupported", "error": "not_decoded", "templates": []})
result = adapter.metadata_object_components({"base_id": "upo_test", "ref": "CommonForm.t_Форма"})
assert [item["component_kind"] for item in result["components"]] == ["metadata_object"]
assert result["unresolved"] == [{"area": "templates", "method": "metadata.object.templates", "status": "unsupported", "error": "not_decoded"}]
+81 -2
View File
@@ -562,7 +562,7 @@ def test_adapter_live_methods_require_base_id(onec_agent_server: dict[str, str])
assert status == 400
assert is_error
assert turn_data["error"]["code"] == "adapter_error"
assert "requires params.base_id" in turn_data["error"]["message"]
assert "forbidden technical fields: table" in turn_data["error"]["message"]
audit_root = Path(onec_agent_server["audit_root"])
turn_files = sorted((audit_root / "turn_audit").glob("*.jsonl"))
@@ -572,10 +572,89 @@ def test_adapter_live_methods_require_base_id(onec_agent_server: dict[str, str])
record["outcome"] == "failure"
and record["failure_type"] == "adapter_error"
and record["error_code"] == "adapter_error"
and "requires params.base_id" in str(record["error_message"])
and "forbidden technical fields: table" in str(record["error_message"])
for record in turn_records
)
def test_agent_adapter_policy_uses_effective_view_and_blocks_storage_selectors(
monkeypatch: pytest.MonkeyPatch,
) -> None:
prepared = onec_agent.prepare_agent_adapter_call(
"metadata.object.full",
{"base_id": "upo_test", "name": "Отчет.Продажи"},
)
assert prepared["configuration_view"] == "effective_working"
assert prepared["source_state"] == "working"
with pytest.raises(ValueError, match="forbidden technical fields: table"):
onec_agent.prepare_agent_adapter_call(
"metadata.saved_state.forms.search",
{"base_id": "upo_test", "table": "ConfigSave"},
)
with pytest.raises(ValueError, match="forbidden technical fields: module_ref"):
onec_agent.prepare_agent_adapter_call(
"code.read",
{"base_id": "upo_test", "selector": {"module_ref": "private"}},
)
monkeypatch.setenv("ONEC_AGENT_ALLOW_DIAGNOSTIC", "true")
assert onec_agent.prepare_agent_adapter_call(
"metadata.saved_state.forms.search",
{"base_id": "upo_test", "table": "ConfigSave"},
)["table"] == "ConfigSave"
def test_agent_calls_adapter_through_mcp(monkeypatch: pytest.MonkeyPatch) -> None:
requests: list[tuple[dict[str, Any], dict[str, str], str]] = []
class FakeResponse:
def __init__(self, body: dict[str, Any], headers: dict[str, str] | None = None) -> None:
self._body = json.dumps(body).encode("utf-8")
self.headers = headers or {}
def read(self) -> bytes:
return self._body
def __enter__(self):
return self
def __exit__(self, *_args: object) -> None:
return None
def fake_urlopen(request: Request, timeout: float):
body = json.loads(request.data.decode("utf-8"))
requests.append((body, dict(request.headers), request.full_url))
if body["method"] == "initialize":
return FakeResponse({"jsonrpc": "2.0", "id": body["id"], "result": {}}, {"Mcp-Session-Id": "test-session"})
return FakeResponse(
{
"jsonrpc": "2.0",
"id": body["id"],
"result": {"content": [{"type": "text", "text": json.dumps({"status": "ok"})}]},
}
)
monkeypatch.setattr(onec_agent, "urlopen", fake_urlopen)
result = onec_agent.call_adapter("metadata.object.forms", {"base_id": "upo_test", "ref": "Report.Тест"})
assert result == {"status": "ok"}
assert len(requests) == 2
assert requests[0][0]["method"] == "initialize"
assert requests[1][0]["method"] == "tools/call"
assert requests[1][0]["params"]["name"] == "onec_request"
assert requests[1][0]["params"]["arguments"]["payload"]["source_state"] == "working"
assert requests[1][2] == "http://docker.cin.su:8021/mcp"
def test_agent_system_prompt_requires_mcp_name_first_navigation() -> None:
prompt = onec_agent.DEFAULT_SYSTEM_PROMPT_PATH.read_text(encoding="utf-8")
assert "только инструментом MCP `onec_request`" in prompt
assert "source_state=working" in prompt
assert "Не проси и не подставляй GUID либо `module_ref`" in prompt
def test_get_chat_runtime(onec_agent_server: dict[str, str]) -> None:
url = onec_agent_server["url"]
File diff suppressed because it is too large Load Diff
+164
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
import sqlite3
import sys
from pathlib import Path
@@ -50,6 +51,80 @@ def configured_layers(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json"))
def test_repositoryless_base_inherits_test_write_profile_to_discovered_extension(monkeypatch, tmp_path: Path) -> None:
config = {
"upo_test": {
"server": "sql", "database": "upo_test", "user": "reader",
"development_layers": {
"base": {
"repository": {"mode": "none", "connection_state": "not_configured"},
"support": {"mode": "unknown"},
},
},
},
}
extension_guid = "11111111-2222-3333-4444-555555555555"
monkeypatch.setenv("ONEC_SQL_BASES_JSON", json.dumps(config))
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(tmp_path / "locks.json"))
payload = {"base_id": "upo_test", "extension_guid": extension_guid}
repository, repository_error = repository_control.repository_config("upo_test", f"extension:{extension_guid}")
assert repository_error is None
assert repository["mode"] == "none"
assert repository["inherited_from_layer"] == "base"
assert repository_control.write_gate(payload)["allowed"] is True
support = repository_control.support_gate(payload)
assert support["allowed"] is True
assert support["status"] == "not_on_support_inherited_no_repository"
def test_repository_policy_is_not_inherited_from_real_base_to_extension(monkeypatch, tmp_path: Path) -> None:
configured_layers(monkeypatch, tmp_path)
repository, error = repository_control.repository_config(
"base", "extension:99999999-2222-3333-4444-555555555555"
)
assert repository is None
assert error["status"] == "layer_not_configured"
def test_repository_state_migrates_once_to_local_sqlite(monkeypatch, tmp_path: Path) -> None:
legacy_path = tmp_path / "locks.json"
state_path = tmp_path / "adapter-state.sqlite"
legacy = {
"requests": {
"rreq-legacy": {
"base_id": "base",
"layer_id": "base",
"status": "cancelled",
"created_at": 1.0,
}
},
"sessions": {},
"audit": [{"event": "legacy_event", "time": 1.0, "base_id": "base"}],
}
legacy_path.write_text(json.dumps(legacy), encoding="utf-8")
original = legacy_path.read_bytes()
monkeypatch.setenv("ONEC_REPOSITORY_STATE_FILE", str(legacy_path))
monkeypatch.setenv("ONEC_ADAPTER_STATE_DB", str(state_path))
state = repository_control._read_state()
assert state["requests"]["rreq-legacy"]["status"] == "cancelled"
assert legacy_path.read_bytes() == original
with sqlite3.connect(state_path) as conn:
assert conn.execute(
"SELECT value FROM adapter_state_meta WHERE key='legacy_repository_state_imported'"
).fetchone()[0] == "1"
assert int(conn.execute(
"SELECT value FROM adapter_state_meta WHERE key='adapter_state_schema_version'"
).fetchone()[0]) >= 2
assert conn.execute("SELECT COUNT(*) FROM repository_lock_requests").fetchone()[0] == 1
assert conn.execute("SELECT COUNT(*) FROM repository_lock_events").fetchone()[0] == 1
def test_repository_backend_and_endpoint_come_only_from_base_settings(monkeypatch, tmp_path: Path) -> None:
configured_base(monkeypatch, tmp_path)
config, error = repository_control.repository_config("base")
@@ -70,6 +145,89 @@ def test_http_runner_profile_does_not_require_local_designer_or_credentials(monk
assert "designer_path" not in configured
def test_activation_debug_probe_calls_dedicated_http_endpoint_without_execution(
monkeypatch,
) -> None:
captured = {}
class Response:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
return json.dumps(
{
"status": "ready",
"runner": {
"kind": "local",
"reachable": True,
"designer_available": True,
"infobase_selector_configured": True,
},
"operation": {
"kind": "/UpdateDBCfg",
"execution_supported": False,
},
"execution": {
"mode": "debug",
"performed": False,
"designer_started": False,
},
"debug_acceptance": {
"accepted": True,
"request_id": "actreq-" + "1" * 32,
"fingerprint": "a" * 64,
"receipt": "b" * 64,
},
}
).encode("utf-8")
def fake_urlopen(request, timeout):
captured["url"] = request.full_url
captured["body"] = json.loads(request.data.decode("utf-8"))
captured["authorization"] = request.headers.get("Authorization")
captured["timeout"] = timeout
return Response()
monkeypatch.setenv("ACTIVATION_RUNNER_TOKEN", "runner-secret")
monkeypatch.setattr(repository_control.urllib.request, "urlopen", fake_urlopen)
result = repository_control.activation_debug_probe(
"upo_test",
{
"runner": {
"kind": "http",
"url": "http://runner:8121",
"token_env": "ACTIVATION_RUNNER_TOKEN",
}
},
layer="base_saved_state",
timeout_seconds=7,
request_id="actreq-" + "1" * 32,
fingerprint="a" * 64,
)
assert result["status"] == "ready"
assert captured == {
"url": "http://runner:8121/configuration/activation/debug",
"body": {
"base_id": "upo_test",
"layer": "base_saved_state",
"mode": "debug",
"request_id": "actreq-" + "1" * 32,
"fingerprint": "a" * 64,
},
"authorization": "Bearer runner-secret",
"timeout": 7,
}
assert result["execution"]["performed"] is False
assert result["execution"]["designer_started"] is False
assert result["debug_acceptance"]["receipt"] == "b" * 64
def test_lock_plan_maps_child_metadata_to_development_owner() -> None:
result = repository_control.lock_plan({"object": "РегистрСведений.Настройки.Реквизит.Код"})
assert result["status"] == "ready"
@@ -121,6 +279,9 @@ def test_manual_lock_request_stays_pending_until_user_confirms_exact_saved_scope
}
pending = repository_control.lock_request_status({"request_id": requested["request_id"]})
assert pending["status"] == "pending_user_lock"
assert pending["base_id"] == "base"
assert pending["objects"] == ["РегистрСведений.Настройки"]
assert pending["native_lock_state"] == "unknown"
assert pending["next_call"] == requested["next_call"]
incomplete = repository_control.confirm_manual_lock({"base_id": "base", "request_id": requested["request_id"]})
@@ -494,6 +655,9 @@ def test_confirm_write_context_can_be_forwarded_under_repository_lock() -> None:
assert context["lock_session_id"] == "rlock-test"
assert context["repository_object"] == "Document.тл_Планировщик.Form.ФормаДокументаНовая"
assert context["layer_id"] == "base"
forwarded = adapter_1c_server.repository_write_context(context)
assert forwarded["layer_id"] == "base"
assert forwarded["lock_session_id"] == "rlock-test"
_context, conflict = adapter_1c_server.normalize_repository_write_context({
"lock_session_id": "rlock-one",
+2 -2
View File
@@ -46,9 +46,9 @@ def test_designer_sql_decoding_policy_keeps_adapter_read_only() -> None:
service = yaml.safe_load((CONNECTOR / "service.yaml").read_text(encoding="utf-8"))
assert policy["scope"]["default_base_id"] == "upo_test"
assert policy["scope"]["adapter_role"] == "sql_observer_and_decoder"
assert policy["scope"]["adapter_role"] == "specification_bound_sql_decoder_and_controlled_saved_state_writer"
assert policy["credentials"]["persistence"] == "forbidden_in_repository"
assert policy["sql_observation"]["adapter_access"] == "read_only"
assert policy["sql_observation"]["adapter_access"] == "sql_only"
assert "direct_application_data_write" in policy["sql_observation"]["forbidden"]
assert policy["xml"]["role"] == "offline_schema_reference_only"
assert "policies/designer-sql-decoding-policy.yaml" in service["contracts"]["policies"]
+112
View File
@@ -0,0 +1,112 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "plugins" / "1c"))
from parser.cas_payload import ( # noqa: E402
classify_payload,
decode_declared_utf8_bsl_prefix,
extract_stream_blocks,
replace_declared_utf8_bsl_prefix_same_width,
stream_header,
structural_stream_blocks_with_data,
)
def test_structural_stream_parser_ignores_header_signature_inside_member() -> None:
fake_header = stream_header(1)
first = b"prefix" + fake_header + b"tail"
payload = b"lead" + stream_header(len(first)) + first + stream_header(3) + b"end"
# The old discovery scanner sees the signature embedded in `first`.
assert len(extract_stream_blocks(payload, include_text=True)) == 3
blocks = structural_stream_blocks_with_data(payload)
assert len(blocks) == 2
assert blocks[0]["data"] == first
assert blocks[1]["data"] == b"end"
def test_declared_utf8_bsl_prefix_excludes_opaque_member_tail() -> None:
bsl = b"\xef\xbb\xbf// marker\r\n"
opaque = b"\x00\x04\x10metadata"
member = bsl + opaque
header = f"\r\n{len(bsl):08x} {len(member):08x} 7fffffff \r\n".encode("ascii")
payload = b"lead" + header + member
decoded = decode_declared_utf8_bsl_prefix(payload, 0)
assert decoded["status"] == "ok"
assert decoded["text"] == "// marker\r\n"
assert decoded["bsl_prefix_bytes"] == len(bsl)
assert decoded["opaque_tail_bytes"] == len(opaque)
def test_classification_uses_declared_utf8_prefix_not_opaque_member_tail() -> None:
bsl = b"\xef\xbb\xbf// source-only\r\n"
opaque = b"\x00metadata-tail-that-is-not-bsl"
member = bsl + opaque
header = f"\r\n{len(bsl):08x} {len(member):08x} 7fffffff \r\n".encode("ascii")
classified = classify_payload(b"lead" + header + member, include_text=True)
stream = classified["stream_blocks"][0]
assert stream["text"] == "// source-only\r\n"
assert stream["declared_utf8_bsl_prefix"]["opaque_tail_bytes"] == len(opaque)
def test_classification_does_not_treat_declared_brace_prefix_as_bsl() -> None:
descriptor = b"\xef\xbb\xbf{3,1,0,\"\",0}"
member = descriptor + b"\x00opaque"
header = f"\r\n{len(descriptor):08x} {len(member):08x} 7fffffff \r\n".encode("ascii")
classified = classify_payload(b"lead" + header + member, include_text=True)
assert "declared_utf8_bsl_prefix" not in classified["stream_blocks"][0]
def test_fixed_width_prefix_replacement_preserves_tail_and_all_other_members() -> None:
first = b"first-member"
old_text = "//Пример - 3\r\n//Пример - 4 "
old_prefix = b"\xef\xbb\xbf" + old_text.encode("utf-8")
opaque = b"\x00opaque-platform-tail\x10\x20"
member = old_prefix + opaque
payload = (
b"lead"
+ stream_header(len(first))
+ first
+ f"\r\n{len(old_prefix):08x} {len(member):08x} 7fffffff \r\n".encode("ascii")
+ member
+ stream_header(3)
+ b"end"
)
replacement, evidence = replace_declared_utf8_bsl_prefix_same_width(
payload,
1,
text="//Тест - 3\r\n//Пример - 4 ",
)
before = structural_stream_blocks_with_data(payload)
after = structural_stream_blocks_with_data(replacement)
assert evidence["padding_bytes"] == len("Пример".encode("utf-8")) - len("Тест".encode("utf-8"))
assert before[0]["data"] == after[0]["data"]
assert before[1]["data"][len(old_prefix) :] == after[1]["data"][len(old_prefix) :]
assert before[2]["data"] == after[2]["data"]
assert decode_declared_utf8_bsl_prefix(replacement, 1)["text"].startswith("//Тест - 3")
def test_fixed_width_prefix_replacement_rejects_growth() -> None:
old_prefix = b"\xef\xbb\xbf// one"
member = old_prefix + b"opaque"
payload = b"lead" + f"\r\n{len(old_prefix):08x} {len(member):08x} 7fffffff \r\n".encode("ascii") + member
try:
replace_declared_utf8_bsl_prefix_same_width(payload, 0, text="// replacement that is longer")
except ValueError as exc:
assert str(exc) == "replacement_declared_bsl_prefix_exceeds_fixed_width"
else:
raise AssertionError("fixed-width codec accepted growth")
+42
View File
@@ -0,0 +1,42 @@
from __future__ import annotations
import sys
from pathlib import Path
CONNECTOR = Path(__file__).resolve().parents[2] / "plugins" / "1c" / "connector"
if str(CONNECTOR) not in sys.path:
sys.path.insert(0, str(CONNECTOR))
from write.registry import registered_handlers, select_write_handler # noqa: E402
from write.context import AdapterWriteContext # noqa: E402
from write.handlers.scheduled_job import execute as execute_scheduled_job # noqa: E402
def test_registry_selects_one_supported_handler() -> None:
assert select_write_handler(target_kind="module").key == "module"
assert select_write_handler(target_kind="форма").key == "form"
assert select_write_handler(target_kind="object", operation="add_attribute").key == "object_member"
assert select_write_handler(target_kind="object", operation="synonym").key == "object_property"
assert select_write_handler(target_kind="anything", is_schedule=True).key == "scheduled_job_schedule"
def test_registry_denies_unknown_target_by_default() -> None:
assert select_write_handler(target_kind="template") is None
def test_registry_exposes_each_typed_handler_once() -> None:
handlers = registered_handlers()
assert [item["key"] for item in handlers] == [
"module", "form", "object_property", "object_member", "scheduled_job_schedule"
]
def test_scheduled_job_handler_uses_explicit_context() -> None:
seen: list[dict] = []
result = execute_scheduled_job(
{"base_id": "upo_test"},
AdapterWriteContext(legacy_scheduled_job_writer=lambda payload: seen.append(payload) or {"status": "planned"}),
)
assert result == {"status": "planned"}
assert seen == [{"base_id": "upo_test"}]