Initial SFERA platform baseline
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# sfera-review-engine
|
||||
|
||||
Deterministic review findings over SIR snapshots.
|
||||
|
||||
Checks include:
|
||||
|
||||
- parser diagnostics and unresolved references;
|
||||
- missing 1C role grants on root objects;
|
||||
- unresolved form command handlers;
|
||||
- empty tabular sections and weak tabular-section schemas;
|
||||
- document posting routines without register writes;
|
||||
- outbound integration endpoints.
|
||||
@@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "sfera-review-engine"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"pydantic>=2.0",
|
||||
"sfera-integration-topology",
|
||||
"sfera-sir",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
@@ -0,0 +1,314 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from integration_topology import IntegrationKind, build_integration_topology
|
||||
from query_intelligence import tables_with_read_write_conflicts
|
||||
from sir import DiagnosticSeverity, EdgeKind, NodeKind, SirSnapshot
|
||||
|
||||
|
||||
class ReviewFinding(BaseModel):
|
||||
finding_id: str
|
||||
title: str
|
||||
severity: DiagnosticSeverity
|
||||
message: str
|
||||
source_path: str | None = None
|
||||
line_start: int | None = None
|
||||
|
||||
|
||||
def review_snapshot(snapshot: SirSnapshot) -> list[ReviewFinding]:
|
||||
findings: list[ReviewFinding] = []
|
||||
nodes_by_lineage = {node.lineage_id: node for node in snapshot.nodes}
|
||||
for diagnostic in snapshot.diagnostics:
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.{diagnostic.diagnostic_id}",
|
||||
title=diagnostic.code,
|
||||
severity=diagnostic.severity,
|
||||
message=diagnostic.message,
|
||||
source_path=diagnostic.source_ref.source_path if diagnostic.source_ref else None,
|
||||
line_start=diagnostic.source_ref.line_start if diagnostic.source_ref else None,
|
||||
)
|
||||
)
|
||||
for reference in snapshot.unresolved_references:
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.{reference.reference_id}",
|
||||
title=f"Unresolved {reference.kind.value.lower()}",
|
||||
severity=DiagnosticSeverity.WARNING,
|
||||
message=f"Unresolved reference to {reference.target_name}",
|
||||
source_path=reference.source_ref.source_path,
|
||||
line_start=reference.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
secured_lineages = {
|
||||
edge.target_lineage
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.GRANTS_ACCESS
|
||||
}
|
||||
access_target_kinds = {
|
||||
NodeKind.CATALOG,
|
||||
NodeKind.DOCUMENT,
|
||||
NodeKind.REGISTER,
|
||||
NodeKind.COMMON_MODULE,
|
||||
NodeKind.EXCHANGE_PLAN,
|
||||
NodeKind.SCHEDULED_JOB,
|
||||
NodeKind.BUSINESS_PROCESS,
|
||||
NodeKind.TASK,
|
||||
}
|
||||
for node in snapshot.nodes:
|
||||
if node.kind in access_target_kinds and node.lineage_id not in secured_lineages:
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.security.unsecured.{node.lineage_id}",
|
||||
title="Missing 1C role access",
|
||||
severity=DiagnosticSeverity.WARNING,
|
||||
message=f"Object {node.qualified_name} has no role access grants",
|
||||
source_path=node.source_ref.source_path,
|
||||
line_start=node.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
handled_command_lineages = {
|
||||
edge.source_lineage
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.HANDLES
|
||||
}
|
||||
for node in snapshot.nodes:
|
||||
if node.kind != NodeKind.COMMAND or node.lineage_id in handled_command_lineages:
|
||||
continue
|
||||
handler_name = _command_handler_name(node.attributes)
|
||||
if not handler_name:
|
||||
continue
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.ui.unresolved_handler.{node.lineage_id}",
|
||||
title="Unresolved 1C command handler",
|
||||
severity=DiagnosticSeverity.WARNING,
|
||||
message=f"Command {node.qualified_name} references missing handler {handler_name}",
|
||||
source_path=node.source_ref.source_path,
|
||||
line_start=node.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
handled_form_handlers = {
|
||||
(edge.source_lineage, str(edge.attributes.get("handler_name", "")).casefold())
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.HANDLES
|
||||
}
|
||||
for node in snapshot.nodes:
|
||||
if node.kind != NodeKind.FORM:
|
||||
continue
|
||||
for _, handler_name in _form_handler_names(node.attributes):
|
||||
if (node.lineage_id, handler_name.casefold()) in handled_form_handlers:
|
||||
continue
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.ui.unresolved_form_handler.{node.lineage_id}.{handler_name}",
|
||||
title="Unresolved 1C form event handler",
|
||||
severity=DiagnosticSeverity.WARNING,
|
||||
message=f"Form {node.qualified_name} references missing event handler {handler_name}",
|
||||
source_path=node.source_ref.source_path,
|
||||
line_start=node.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
findings.extend(_document_posting_write_review(snapshot, nodes_by_lineage))
|
||||
tabular_sections_with_columns = {
|
||||
edge.source_lineage
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.HAS_ATTRIBUTE
|
||||
}
|
||||
tabular_section_columns: dict[str, list] = {}
|
||||
for edge in snapshot.edges:
|
||||
if edge.kind != EdgeKind.HAS_ATTRIBUTE:
|
||||
continue
|
||||
source = nodes_by_lineage.get(edge.source_lineage)
|
||||
target = nodes_by_lineage.get(edge.target_lineage)
|
||||
if source is None or target is None:
|
||||
continue
|
||||
if source.kind == NodeKind.TABULAR_SECTION and target.kind == NodeKind.ATTRIBUTE:
|
||||
tabular_section_columns.setdefault(source.lineage_id, []).append(target)
|
||||
for node in snapshot.nodes:
|
||||
if node.kind != NodeKind.TABULAR_SECTION or node.lineage_id in tabular_sections_with_columns:
|
||||
continue
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.schema.empty_tabular_section.{node.lineage_id}",
|
||||
title="Empty 1C tabular section",
|
||||
severity=DiagnosticSeverity.WARNING,
|
||||
message=f"Tabular section {node.qualified_name} has no columns",
|
||||
source_path=node.source_ref.source_path,
|
||||
line_start=node.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
for node in snapshot.nodes:
|
||||
if node.kind != NodeKind.TABULAR_SECTION:
|
||||
continue
|
||||
columns = tabular_section_columns.get(node.lineage_id, [])
|
||||
if not columns or _has_subject_column(columns):
|
||||
continue
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.schema.no_subject_column.{node.lineage_id}",
|
||||
title="No subject column in 1C tabular section",
|
||||
severity=DiagnosticSeverity.INFO,
|
||||
message=(
|
||||
f"Tabular section {node.qualified_name} has columns, "
|
||||
"but no common subject column such as Номенклатура, Товар, Услуга, Объект, or Ссылка"
|
||||
),
|
||||
source_path=node.source_ref.source_path,
|
||||
line_start=node.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
for endpoint in build_integration_topology(snapshot).endpoints:
|
||||
if endpoint.direction == "OUTBOUND" and endpoint.kind in {
|
||||
IntegrationKind.HTTP_SERVICE,
|
||||
IntegrationKind.WEB_SERVICE,
|
||||
IntegrationKind.COM_CONNECTOR,
|
||||
}:
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.integration.{endpoint.endpoint_id}",
|
||||
title="External integration endpoint",
|
||||
severity=DiagnosticSeverity.INFO,
|
||||
message=f"{endpoint.kind.value} integration {endpoint.name} is used by {endpoint.owner}",
|
||||
source_path=None,
|
||||
line_start=None,
|
||||
)
|
||||
)
|
||||
for usage in tables_with_read_write_conflicts(snapshot):
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.query.read_write_conflict.{usage.table.lineage_id}",
|
||||
title="Register read/write dependency",
|
||||
severity=DiagnosticSeverity.INFO,
|
||||
message=(
|
||||
f"{usage.table.qualified_name} is read by "
|
||||
f"{', '.join(reader.name for reader in usage.readers)} and written by "
|
||||
f"{', '.join(writer.name for writer in usage.writers)}"
|
||||
),
|
||||
source_path=usage.table.source_ref.source_path,
|
||||
line_start=usage.table.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _document_posting_write_review(
|
||||
snapshot: SirSnapshot,
|
||||
nodes_by_lineage: dict[str, object],
|
||||
) -> list[ReviewFinding]:
|
||||
module_lineages_by_document = {
|
||||
edge.source_lineage: edge.target_lineage
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.CONTAINS
|
||||
and nodes_by_lineage.get(edge.source_lineage) is not None
|
||||
and getattr(nodes_by_lineage[edge.source_lineage], "kind", None) == NodeKind.DOCUMENT
|
||||
and nodes_by_lineage.get(edge.target_lineage) is not None
|
||||
and getattr(nodes_by_lineage[edge.target_lineage], "kind", None) == NodeKind.MODULE
|
||||
}
|
||||
routines_by_module: dict[str, list] = {}
|
||||
routines_with_writes = {
|
||||
edge.source_lineage
|
||||
for edge in snapshot.edges
|
||||
if edge.kind == EdgeKind.WRITES
|
||||
}
|
||||
for edge in snapshot.edges:
|
||||
if edge.kind != EdgeKind.DECLARES:
|
||||
continue
|
||||
routine = nodes_by_lineage.get(edge.target_lineage)
|
||||
if routine is None or getattr(routine, "kind", None) not in {NodeKind.PROCEDURE, NodeKind.FUNCTION}:
|
||||
continue
|
||||
routines_by_module.setdefault(edge.source_lineage, []).append(routine)
|
||||
|
||||
findings: list[ReviewFinding] = []
|
||||
for document_lineage, module_lineage in module_lineages_by_document.items():
|
||||
document = nodes_by_lineage[document_lineage]
|
||||
posting_routines = [
|
||||
routine
|
||||
for routine in routines_by_module.get(module_lineage, [])
|
||||
if routine.name.casefold() in {"проведение", "posting"}
|
||||
]
|
||||
for routine in posting_routines:
|
||||
if routine.lineage_id in routines_with_writes:
|
||||
continue
|
||||
findings.append(
|
||||
ReviewFinding(
|
||||
finding_id=f"finding.document.posting_no_writes.{routine.lineage_id}",
|
||||
title="Document posting has no register writes",
|
||||
severity=DiagnosticSeverity.INFO,
|
||||
message=(
|
||||
f"Document {document.qualified_name} posting routine "
|
||||
f"{routine.name} does not write registers"
|
||||
),
|
||||
source_path=routine.source_ref.source_path,
|
||||
line_start=routine.source_ref.line_start,
|
||||
)
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def _command_handler_name(attributes: dict) -> str:
|
||||
for key in (
|
||||
"action",
|
||||
"Action",
|
||||
"handler",
|
||||
"Handler",
|
||||
"method",
|
||||
"Method",
|
||||
"methodName",
|
||||
"MethodName",
|
||||
"Действие",
|
||||
"Обработчик",
|
||||
"Метод",
|
||||
"ИмяМетода",
|
||||
):
|
||||
value = attributes.get(key)
|
||||
if value:
|
||||
return str(value).split(".")[-1]
|
||||
return ""
|
||||
|
||||
|
||||
def _form_handler_names(attributes: dict) -> list[tuple[str, str]]:
|
||||
handler_keys = {
|
||||
"oncreate",
|
||||
"onopen",
|
||||
"onclose",
|
||||
"beforeclose",
|
||||
"beforewrite",
|
||||
"afterwrite",
|
||||
"onread",
|
||||
"onchange",
|
||||
"event",
|
||||
"handler",
|
||||
"method",
|
||||
"methodname",
|
||||
"присозданиинсервере",
|
||||
"присозданиинаклиенте",
|
||||
"приоткрытии",
|
||||
"передзакрытием",
|
||||
"призакрытии",
|
||||
"передзаписью",
|
||||
"призаписи",
|
||||
"причтении",
|
||||
"приизменении",
|
||||
"событие",
|
||||
"обработчик",
|
||||
"метод",
|
||||
"имяметода",
|
||||
}
|
||||
handlers: list[tuple[str, str]] = []
|
||||
for key, value in attributes.items():
|
||||
if value and str(key).casefold() in handler_keys:
|
||||
handlers.append((str(key), str(value).split(".")[-1]))
|
||||
return handlers
|
||||
|
||||
|
||||
def _has_subject_column(columns: list) -> bool:
|
||||
subject_names = {"номенклатура", "товар", "товары", "услуга", "услуги", "объект", "ссылка"}
|
||||
for column in columns:
|
||||
name = column.name.casefold()
|
||||
if name in subject_names:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ["ReviewFinding", "review_snapshot"]
|
||||
@@ -0,0 +1,246 @@
|
||||
from review_engine import review_snapshot
|
||||
from semantic_kernel import index_project
|
||||
from sir import ReferenceKind, SirSnapshot, SourceRef, UnresolvedReference
|
||||
|
||||
|
||||
def test_review_snapshot_reports_unresolved_references():
|
||||
snapshot = SirSnapshot(
|
||||
snapshot_id="snapshot.demo",
|
||||
project_id="demo",
|
||||
unresolved_references=[
|
||||
UnresolvedReference(
|
||||
reference_id="ref.1",
|
||||
kind=ReferenceKind.CALL,
|
||||
source_lineage="lineage.procedure.demo",
|
||||
target_name="Missing",
|
||||
source_ref=SourceRef(source_path="module.bsl", line_start=2),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert findings[0].title == "Unresolved call"
|
||||
assert findings[0].source_path == "module.bsl"
|
||||
|
||||
|
||||
def test_review_snapshot_reports_1c_objects_without_role_access(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-security")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert findings[0].title == "Missing 1C role access"
|
||||
assert "Документ.ЗаказПокупателя" in findings[0].message
|
||||
|
||||
|
||||
def test_review_snapshot_reports_external_integrations(tmp_path):
|
||||
module = tmp_path / "integration.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Отправить()
|
||||
Адрес = "https://api.example.local/orders";
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-integrations")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(finding.title == "External integration endpoint" for finding in findings)
|
||||
|
||||
|
||||
def test_review_snapshot_reports_unresolved_1c_command_handler(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
||||
<Form name="ФормаДокумента" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента">
|
||||
<Command name="Провести" qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента.Провести" action="ПровестиКоманда" />
|
||||
</Form>
|
||||
</Document>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-ui-handler")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(
|
||||
finding.title == "Unresolved 1C command handler"
|
||||
and "ПровестиКоманда" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
|
||||
|
||||
def test_review_snapshot_reports_unresolved_1c_form_event_handler(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
||||
<Form
|
||||
name="ФормаДокумента"
|
||||
qualifiedName="Документ.ЗаказПокупателя.ФормаДокумента"
|
||||
onCreate="ПриСозданииНаСервере"
|
||||
beforeWrite="ПередЗаписью"
|
||||
/>
|
||||
</Document>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "form_module.bsl"
|
||||
module.write_text("Процедура ПриСозданииНаСервере()\nКонецПроцедуры\n", encoding="utf-8")
|
||||
snapshot = index_project(tmp_path, project_id="review-form-handler")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(
|
||||
finding.title == "Unresolved 1C form event handler"
|
||||
and "ПередЗаписью" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
assert not any(
|
||||
finding.title == "Unresolved 1C form event handler"
|
||||
and "ПриСозданииНаСервере" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
|
||||
|
||||
def test_review_snapshot_reports_empty_1c_tabular_section(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
||||
<TabularSection name="Услуги" qualifiedName="Документ.ЗаказПокупателя.Услуги" />
|
||||
</Document>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-empty-tabular-section")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(
|
||||
finding.title == "Empty 1C tabular section"
|
||||
and "Документ.ЗаказПокупателя.Услуги" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
|
||||
|
||||
def test_review_snapshot_reports_tabular_section_without_subject_column(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
||||
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
||||
<Attribute name="Количество" qualifiedName="Документ.ЗаказПокупателя.Товары.Количество" />
|
||||
</TabularSection>
|
||||
</Document>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-no-subject-column")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(
|
||||
finding.title == "No subject column in 1C tabular section"
|
||||
and finding.severity.value == "INFO"
|
||||
and "Документ.ЗаказПокупателя.Товары" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
|
||||
|
||||
def test_review_snapshot_accepts_tabular_section_with_subject_column(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя">
|
||||
<TabularSection name="Товары" qualifiedName="Документ.ЗаказПокупателя.Товары">
|
||||
<Attribute name="Номенклатура" qualifiedName="Документ.ЗаказПокупателя.Товары.Номенклатура" />
|
||||
<Attribute name="Количество" qualifiedName="Документ.ЗаказПокупателя.Товары.Количество" />
|
||||
</TabularSection>
|
||||
</Document>
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-subject-column")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert not any(finding.title == "No subject column in 1C tabular section" for finding in findings)
|
||||
|
||||
|
||||
def test_review_snapshot_reports_document_posting_without_register_writes(tmp_path):
|
||||
xml = tmp_path / "metadata.xml"
|
||||
xml.write_text(
|
||||
"""
|
||||
<Configuration>
|
||||
<Document name="ЗаказПокупателя" qualifiedName="Документ.ЗаказПокупателя" />
|
||||
</Configuration>
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl"
|
||||
module.parent.mkdir(parents=True)
|
||||
module.write_text("Процедура Проведение()\nКонецПроцедуры\n", encoding="utf-8")
|
||||
snapshot = index_project(tmp_path, project_id="review-document-posting")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(
|
||||
finding.title == "Document posting has no register writes"
|
||||
and finding.severity.value == "INFO"
|
||||
and "Документ.ЗаказПокупателя" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
|
||||
|
||||
def test_review_snapshot_reports_register_read_write_dependency(tmp_path):
|
||||
module = tmp_path / "module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура ПроверитьОстатки()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст =
|
||||
"ВЫБРАТЬ
|
||||
Остатки.Номенклатура
|
||||
ИЗ
|
||||
РегистрНакопления.ОстаткиТоваров КАК Остатки";
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура Проведение()
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="review-query-conflict")
|
||||
|
||||
findings = review_snapshot(snapshot)
|
||||
|
||||
assert any(
|
||||
finding.title == "Register read/write dependency"
|
||||
and "ПроверитьОстатки" in finding.message
|
||||
and "Проведение" in finding.message
|
||||
for finding in findings
|
||||
)
|
||||
Reference in New Issue
Block a user