Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions datadog_sync/model/notebooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,20 @@ async def delete_resource(self, _id: str) -> None:
self.resource_config.base_path + f"/{self.config.state.destination[self.resource_type][_id]['id']}"
)

# Server-managed AI usage tag keys injected by the Notebooks API on every write.
# These reflect per-org interaction history (MCP vs human), not notebook content,
# and must be stripped to avoid non-converging diffs during sync.
_ai_usage_tag_keys = frozenset({"ai_generated", "ai_edited", "human_edited"})

@staticmethod
def handle_special_case_attr(resource):
# Handle template_variables attribute
if "template_variables" in resource["attributes"] and not resource["attributes"]["template_variables"]:
resource["attributes"].pop("template_variables")

# Strip server-managed AI usage tags
tags = resource["attributes"].get("tags")
if tags:
resource["attributes"]["tags"] = [
t for t in tags if t.split(":")[0] not in Notebooks._ai_usage_tag_keys
]
8 changes: 8 additions & 0 deletions datadog_sync/model/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,17 @@ async def remap_permissions(self, resource):
return

if "permissions" in resource["relationships"]:
matched_permissions = []
for permission in resource["relationships"]["permissions"]["data"]:
if permission["id"] in self.destination_permissions:
permission["id"] = self.destination_permissions[permission["id"]]
matched_permissions.append(permission)
else:
self.config.logger.warning(
"permission '%s' exists in source but not in destination, skipping",
permission["id"],
)
resource["relationships"]["permissions"]["data"] = matched_permissions

async def get_destination_roles_mapping(self):
destination_client = self.config.destination_client
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def test_resource_update_sync(self, runner, caplog):
]

if self.filter:
diff_cmd.append(f"--filter={self.filter}")
sync_cmd.append(f"--filter={self.filter}")

if self.resource_per_file:
sync_cmd.append("--resource-per-file")
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/test_helpers_command_building.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Unless explicitly stated otherwise all files in this repository are licensed
# under the 3-clause BSD style license (see LICENSE).
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2019 Datadog, Inc.

"""Regression tests for CLI command-building logic in BaseResourcesTestClass.

Validates that filter arguments are appended to the correct command lists
when test helper methods construct CLI invocations. See: helpers.py line 242.
"""

import pytest


def _build_update_sync_commands(resource_type, filter_value, resource_per_file=False):
"""Reproduce the command-building logic from test_resource_update_sync (helpers.py:211-246).

Returns (diff_cmd, sync_cmd) as built by that method.
"""
diff_cmd = [
"diffs",
"--validate=false",
"--verify-ddr-status=False",
f"--resources={resource_type}",
"--send-metrics=False",
]

if filter_value:
diff_cmd.append(f"--filter={filter_value}")

if resource_per_file:
diff_cmd.append("--resource-per-file")

sync_cmd = [
"sync",
"--validate=false",
"--verify-ddr-status=False",
f"--resources={resource_type}",
"--create-global-downtime=False",
"--send-metrics=False",
]

if filter_value:
sync_cmd.append(f"--filter={filter_value}")

if resource_per_file:
sync_cmd.append("--resource-per-file")

return diff_cmd, sync_cmd


@pytest.mark.parametrize(
"filter_value",
[
"Type=logs_pipelines;Name=is_read_only;Value=false",
"Type=monitors;Name=tags;Value=sync:true",
],
)
def test_filter_appended_to_sync_cmd(filter_value):
"""Regression: --filter must appear in sync_cmd, not only in diff_cmd."""
diff_cmd, sync_cmd = _build_update_sync_commands("test_resource", filter_value)

filter_arg = f"--filter={filter_value}"
assert filter_arg in sync_cmd, f"sync_cmd missing filter: {sync_cmd}"
assert sync_cmd.count(filter_arg) == 1, f"sync_cmd has duplicate filter: {sync_cmd}"


@pytest.mark.parametrize(
"filter_value",
[
"Type=logs_pipelines;Name=is_read_only;Value=false",
"Type=monitors;Name=tags;Value=sync:true",
],
)
def test_filter_appears_once_in_diff_cmd(filter_value):
"""Regression: --filter must appear exactly once in diff_cmd."""
diff_cmd, sync_cmd = _build_update_sync_commands("test_resource", filter_value)

filter_arg = f"--filter={filter_value}"
assert filter_arg in diff_cmd, f"diff_cmd missing filter: {diff_cmd}"
assert diff_cmd.count(filter_arg) == 1, f"diff_cmd has duplicate filter: {diff_cmd}"


def test_no_filter_when_empty():
"""When filter is empty, neither command should contain --filter."""
diff_cmd, sync_cmd = _build_update_sync_commands("test_resource", "")

for cmd in [diff_cmd, sync_cmd]:
assert not any(arg.startswith("--filter=") for arg in cmd), f"Unexpected filter in: {cmd}"
Loading