Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
97c058f
FEATURE: Use internal content-stream-id alias for hierarchies (Introd…
mhsdesign Feb 1, 2026
3c56094
WIP: Introduce ContentStreamDbIds to handle one cs with multiple db ids
mhsdesign Apr 10, 2026
7dd602c
WIP: Copy hierarchy relations on write
mhsdesign Apr 12, 2026
c7793f9
WIP: Copy hierarchy relations on write (node variation)
mhsdesign Apr 12, 2026
e883ac1
WIP: Node variation in `07-NodeRemoval/04-VariantRecreation.feature:48`
mhsdesign Apr 12, 2026
aaf4d2f
TASK: Rename temporary `ContentStreamDbId` to `ContentStreamLayer`(s)…
mhsdesign Apr 14, 2026
ae90b18
TASK: Refactor `HierarchyRelationStatement` to value object
mhsdesign Apr 14, 2026
309c7eb
TASK: Refactor `HierarchyRelationStatement` to value object
mhsdesign Apr 14, 2026
4eaf82f
TASK: Refactor ContentStreamLayers::current
mhsdesign Apr 14, 2026
b9c7caa
WIP: SubtreeTagging with sql upsert
mhsdesign Apr 15, 2026
a188622
WIP: use HierarchyRelationStatement on all readside queries
mhsdesign Apr 15, 2026
2416b3a
TASK: Copy on write during move
mhsdesign Apr 15, 2026
1058eac
PATCH: Add missing alias
mhsdesign Apr 15, 2026
90d44b4
PATCH: Adjust remaining cases in ProjectionContentGraph to content st…
mhsdesign Apr 15, 2026
50fba89
PATCH: Re-enable reference copying on node copy on write
mhsdesign Apr 15, 2026
9a488b0
TASK: Implement dimension space point migration events with copy on w…
mhsdesign Apr 15, 2026
984dfc2
PATCH: Fix findNodeAggregatesTaggedBy
mhsdesign Apr 15, 2026
fbfc972
WIP: Migrate `ProjectionIntegrityViolationDetector` to content stream…
mhsdesign Apr 15, 2026
69ca97d
PATCH: Ensure query returns only rows where origin is covered not nul…
mhsdesign Apr 16, 2026
78931ee
WIP: Add failing test for content stream layer integrity violation
mhsdesign Apr 16, 2026
0430f19
TASK: Implement contentStreamLayersIntegrityIsProvided for redundant …
mhsdesign Apr 16, 2026
7afd49a
TASK: Implement merging layers during content stream removal
mhsdesign Apr 16, 2026
008ba82
PATCH: Adjust variable name
mhsdesign Apr 16, 2026
2e60d1d
TASK: Remove hierarchies of active write layer when content stream wa…
mhsdesign Apr 16, 2026
86b22a1
PATCH: Remove hierarchies from merged layer
mhsdesign Apr 16, 2026
e54e1ad
TASK: Dont use new write layer during forking if current one is unwri…
mhsdesign Apr 16, 2026
9d64003
TASK: Reintroduce node dropping when hierarchy rows are removed
mhsdesign Apr 16, 2026
ba982a7
PATCH: Do dont create layer if root layer does not contain any changes
mhsdesign Apr 16, 2026
0971b99
TASK: Unset all columns during hierarchy removal in layer
mhsdesign Apr 17, 2026
d3e10a6
PATCH: Use `HierarchyRelationStatement` without content stream filter…
mhsdesign Apr 17, 2026
c6fe201
TASK: Ensure content stream merging only applies if all parent layers…
mhsdesign Apr 17, 2026
674fc16
TASK: Adjust `ProjectionIntegrityViolationDetectionTrait` to content …
mhsdesign Apr 21, 2026
d90a91a
TASK: Correct selecting the combination of all hierarchies for all co…
mhsdesign Apr 24, 2026
53128f3
PATCH: Improve error for missing hierarchies for node rows
mhsdesign Apr 24, 2026
d028af8
TASK: When merging into the root layer remove hierarchies which acted…
mhsdesign Apr 25, 2026
f8bb2be
TASK: Reintroduce dropping of node references during content stream r…
mhsdesign Apr 25, 2026
0dd5afe
PATCH: Fix phpstan for content stream layer merging
mhsdesign Apr 25, 2026
69fd13b
TASK: Optimize queries to use ORDER BY LIMIT 1 instead of MAX()
mhsdesign Apr 27, 2026
0dbf1ad
TASK: Fix move of subtree tagging to only update / insert hierarchies…
mhsdesign May 6, 2026
8dec6d4
BUGFIX: Take layers into account for hierarchy relations for subtree …
mhsdesign May 6, 2026
b6fbfc3
TASK: Be correct and always use `HierarchyRelationStatement` if appli…
mhsdesign May 6, 2026
e38a6a1
TASK: Mysql compatibility by using JSON type comparison instead of `J…
mhsdesign May 6, 2026
6858eb5
TASK: Remove obsolete `$restrictContentStreamLayers` flag
mhsdesign May 11, 2026
93e5040
TASK: Cleanup legacy selects ala `h.contentstreamlayer` when fetching…
mhsdesign May 12, 2026
28fa591
TASK: Optimize hierarchy layer calculation via force index
mhsdesign May 12, 2026
211c156
TASK: Optimize slow determine hierarchy position further
mhsdesign May 12, 2026
3f39adb
TASK: Optimize SQL for `getContentGraph()`
mhsdesign May 12, 2026
9d9cd4d
TASK: Provide tests that subtree tag moving does not create obsolete …
mhsdesign May 13, 2026
078ba66
TASK: Avoid removeSubtreeTag to copy hierarchies for still tagged des…
mhsdesign May 13, 2026
50696f4
TASK: Apply proper formatting to SQL subtree tag queries
mhsdesign May 13, 2026
ad6d58f
PATH: Remove obsolete comment
mhsdesign May 13, 2026
e2f54f0
TASK: Optimise `buildBackreferencesQuery` by adding missing LIMIT 1
mhsdesign May 13, 2026
c6c639d
PATCH: Remove obsolete comments
mhsdesign May 13, 2026
8b18b46
PATCH: Apply formatting and HEREDOC indentation to all new SQL snippets
mhsdesign May 13, 2026
851d25a
TASK: Apply review suggestions and fine tune ContentStreamLayers
mhsdesign May 18, 2026
64c7ff0
PATCH: Correct naming of `HierarchyRelationId` variable
mhsdesign May 18, 2026
8861564
TASK: Replace `ContentStreamLayerTrait` in favor of actual `ContentSt…
mhsdesign May 23, 2026
01ad0fa
TASK: Reference integrity checks to content stream layers
mhsdesign May 23, 2026
8b900bb
TASK: Don't introduce new auto-increment column for hierarchy relatio…
mhsdesign May 21, 2026
66853c2
WIP: Use `CREATE TRIGGER` to populate rows for live workspace hierarc…
mhsdesign May 11, 2026
bf9b3bd
WIP: Use after `apply` to insert added hierarchy relation ids
mhsdesign May 11, 2026
7045360
PATCH: Add missing index
mhsdesign May 11, 2026
053bee4
WIP: Revert to trigger approach
mhsdesign May 23, 2026
b3a8454
TASK: Add delete trigger and correctly scope triggers to CR
mhsdesign May 23, 2026
d1de5ae
WIP: revert wrong cs removal change
mhsdesign May 24, 2026
042349b
WIP: add update hook (does not work)
mhsdesign May 24, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,26 @@

declare(strict_types=1);

use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames;
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Projection\ContentStreamLayer;
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\NodeFactory;
use Neos\ContentGraph\DoctrineDbalAdapter\Tests\Behavior\Features\Bootstrap\Helpers\TestingNodeAggregateId;
use Neos\ContentRepository\Core\DimensionSpace\DimensionSpacePoint;
use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint;
use Neos\ContentRepository\Core\Feature\SubtreeTagging\Dto\SubtreeTag;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\ProjectionIntegrityViolationDetectionTrait;
use PHPUnit\Framework\Assert;

/**
* @internal custom illegal mutations for the Doctrine DBAL content graph adapter to make the projection integrity violation fail
*/
trait DoctrineDbalProjectionIntegrityViolatorTrait
{
use CRTestSuiteRuntimeVariables;

private Connection $dbal;

/**
* @template T of object
* @param class-string<T> $className
Expand All @@ -43,16 +40,19 @@ trait DoctrineDbalProjectionIntegrityViolatorTrait
*/
abstract private function getObject(string $className): object;

private function tableNames(): ContentGraphTableNames
/**
* @When the content stream :contentStreamId was removed without layer cleanup
* @throws DBALException
*/
public function theContentStreamWasRemovedWithoutLayerCleanup(string $contentStreamId): void
{
return ContentGraphTableNames::create(
$this->currentContentRepository->id
);
}
$this->dbal->delete($this->tableNames()->contentStream(), [
'id' => $contentStreamId
]);

public function setupDbalGraphAdapterIntegrityViolationTrait()
{
$this->dbal = $this->getObject(Connection::class);
$this->dbal->delete($this->tableNames()->contentStreamLayer(), [
'contentStreamId' => $contentStreamId,
]);
}

/**
Expand Down Expand Up @@ -92,7 +92,10 @@ public function iAddTheFollowingHierarchyRelation(TableNode $payloadTable): void
$record = $this->transformDatasetToHierarchyRelationRecord($dataset);
$this->dbal->insert(
$this->tableNames()->hierarchyRelation(),
$record
[
'id' => 1000, // FIXME should not be hardcoded if two hierarchies are to be added for testing
...$record
]
);
}

Expand All @@ -108,7 +111,9 @@ public function iChangeTheFollowingHierarchyRelationsParent(TableNode $payloadTa
unset($record['subtreetags']);

$newParentHierarchyRelation = $this->findHierarchyRelationByIds(
ContentStreamId::fromString($dataset['contentStreamId']),
$this->requireSingleWriteLayer(
ContentStreamId::fromString($dataset['contentStreamId'])
),
DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']),
NodeAggregateId::fromString($dataset['newParentNodeAggregateId'])
);
Expand Down Expand Up @@ -152,14 +157,18 @@ public function iChangeTheFollowingNodesName(TableNode $payloadTable): void
{
$dataset = $this->transformPayloadTableToDataset($payloadTable);

$contentStreamLayer = $this->requireSingleWriteLayer(
ContentStreamId::fromString($dataset['contentStreamId'])
);

$relationAnchorPoint = $this->dbal->executeQuery(
'SELECT n.relationanchorpoint FROM ' . $this->tableNames()->node() . ' n
JOIN ' . $this->tableNames()->hierarchyRelation() . ' h ON h.childnodeanchor = n.relationanchorpoint
WHERE h.contentstreamid = :contentStreamId
WHERE h.contentstreamlayer = :contentStreamLayer
AND n.nodeaggregateId = :nodeAggregateId
AND n.origindimensionspacepointhash = :originDimensionSpacePointHash',
[
'contentStreamId' => $dataset['contentStreamId'],
'contentStreamLayer' => $contentStreamLayer->value,
'nodeAggregateId' => $dataset['nodeAggregateId'],
'originDimensionSpacePointHash' => OriginDimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'])->hash,
]
Expand All @@ -185,8 +194,13 @@ public function iSetTheFollowingPosition(TableNode $payloadTable): void
{
$dataset = $this->transformPayloadTableToDataset($payloadTable);
$dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']);

$contentStreamLayer = $this->requireSingleWriteLayer(
ContentStreamId::fromString($dataset['contentStreamId'])
);

$record = [
'contentstreamid' => $dataset['contentStreamId'],
'contentstreamlayer' => $contentStreamLayer->value,
'dimensionspacepointhash' => $dimensionSpacePoint->hash,
'childnodeanchor' => $this->findRelationAnchorPointByDataset($dataset)
];
Expand Down Expand Up @@ -241,7 +255,9 @@ private function transformDatasetToReferenceRelationRecord(array $dataset): arra
return [
'name' => $dataset['referenceName'],
'nodeanchorpoint' => $this->findHierarchyRelationByIds(
ContentStreamId::fromString($dataset['contentStreamId']),
$this->requireSingleWriteLayer(
ContentStreamId::fromString($dataset['contentStreamId'])
),
DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']),
NodeAggregateId::fromString($dataset['sourceNodeAggregateId'])
)['childnodeanchor'],
Expand All @@ -254,25 +270,28 @@ private function transformDatasetToHierarchyRelationRecord(array $dataset): arra
$dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['dimensionSpacePoint']);
$parentNodeAggregateId = TestingNodeAggregateId::fromString($dataset['parentNodeAggregateId']);
$childAggregateId = TestingNodeAggregateId::fromString($dataset['childNodeAggregateId']);
$contentStreamLayer = $this->requireSingleWriteLayer(
ContentStreamId::fromString($dataset['contentStreamId'])
);

$parentHierarchyRelation = $parentNodeAggregateId->isNonExistent()
? null
: $this->findHierarchyRelationByIds(
ContentStreamId::fromString($dataset['contentStreamId']),
$contentStreamLayer,
$dimensionSpacePoint,
NodeAggregateId::fromString($dataset['parentNodeAggregateId'])
);

$childHierarchyRelation = $childAggregateId->isNonExistent()
? null
: $this->findHierarchyRelationByIds(
ContentStreamId::fromString($dataset['contentStreamId']),
$contentStreamLayer,
$dimensionSpacePoint,
NodeAggregateId::fromString($dataset['childNodeAggregateId'])
);

return [
'contentstreamid' => $dataset['contentStreamId'],
'contentstreamlayer' => $contentStreamLayer->value,
'dimensionspacepointhash' => $dimensionSpacePoint->hash,
'parentnodeanchor' => $parentHierarchyRelation !== null ? $parentHierarchyRelation['childnodeanchor'] : 9999999,
'childnodeanchor' => $childHierarchyRelation !== null ? $childHierarchyRelation['childnodeanchor'] : 8888888,
Expand All @@ -286,14 +305,16 @@ private function findRelationAnchorPointByDataset(array $dataset): int
$dimensionSpacePoint = DimensionSpacePoint::fromArray($dataset['originDimensionSpacePoint'] ?? $dataset['dimensionSpacePoint']);

return $this->findHierarchyRelationByIds(
ContentStreamId::fromString($dataset['contentStreamId']),
$this->requireSingleWriteLayer(
ContentStreamId::fromString($dataset['contentStreamId'])
),
$dimensionSpacePoint,
NodeAggregateId::fromString($dataset['nodeAggregateId'] ?? $dataset['childNodeAggregateId'])
)['childnodeanchor'];
}

private function findHierarchyRelationByIds(
ContentStreamId $contentStreamId,
ContentStreamLayer $contentStreamLayer,
DimensionSpacePoint $dimensionSpacePoint,
NodeAggregateId $nodeAggregateId
): array {
Expand All @@ -302,17 +323,17 @@ private function findHierarchyRelationByIds(
FROM ' . $this->tableNames()->node() . ' n
INNER JOIN ' . $this->tableNames()->hierarchyRelation() . ' h
ON n.relationanchorpoint = h.childnodeanchor
WHERE n.nodeaggregateid = :nodeAggregateId
AND h.contentstreamid = :contentStreamId
WHERE h.contentstreamlayer = :contentStreamLayer
AND n.nodeaggregateid = :nodeAggregateId
AND h.dimensionspacepointhash = :dimensionSpacePointHash',
[
'contentStreamId' => $contentStreamId->value,
'contentStreamLayer' => $contentStreamLayer->value,
'dimensionSpacePointHash' => $dimensionSpacePoint->hash,
'nodeAggregateId' => $nodeAggregateId->value
]
)->fetchAssociative();
if ($nodeRecord === false) {
throw new \InvalidArgumentException(sprintf('Failed to find hierarchy relation for content stream "%s", dimension space point "%s" and node aggregate id "%s"', $contentStreamId->value, $dimensionSpacePoint->hash, $nodeAggregateId->value), 1708617712);
throw new \InvalidArgumentException(sprintf('Failed to find hierarchy relation for content stream "%s", dimension space point "%s" and node aggregate id "%s"', $contentStreamLayer->value, $dimensionSpacePoint->hash, $nodeAggregateId->value), 1708617712);
}

return $nodeRecord;
Expand All @@ -327,4 +348,38 @@ private function transformPayloadTableToDataset(TableNode $payloadTable): array

return $result;
}

private function requireSingleWriteLayer(ContentStreamId $contentStreamId): ContentStreamLayer
{
$contentStreamLayers = $this->contentStreamLayerFinder()->getContentStreamLayers($contentStreamId);

if (!$contentStreamLayers->getWriteLayer()->equals($contentStreamLayers->getRootLayer())) {
throw new \RuntimeException(sprintf('For testing a single write layer is currently only supported for modification. Got %s for content stream %s.', $contentStreamLayers->toDebugString(), $contentStreamId->value), 1776786186);
}

return $contentStreamLayers->getWriteLayer();
}

/**
* DBAL Adapter specific assertion. The Error message strongly varies from adapter to adapter. Thus, we extend
*
* {@see ProjectionIntegrityViolationDetectionTrait::iExpectIntegrityViolationDetectionResultErrorNumberNToHaveCodeX} here.
*
* @Then I expect integrity violation detection result error number :errorNumber to have code :expectedErrorCode and message:
* @param int $errorNumber
* @param int $expectedErrorCode
*/
public function iExpectIntegrityViolationDetectionResultErrorNumberNToHaveCodeXAndDbalAdapterMessage(int $errorNumber, int $expectedErrorCode, PyStringNode $message): void
{
$this->iExpectIntegrityViolationDetectionResultErrorNumberNToHaveCodeX($errorNumber, $expectedErrorCode);

/** @var \Neos\Error\Messages\Error $error */
$error = $this->lastIntegrityViolationDetectionResult->getErrors()[$errorNumber-1];

Assert::assertSame(
$message->getRaw(),
$error->render(),
"[{$error->getCode()}] " . $error->render()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

use PHPUnit\Framework\Assert;

/**
* @internal custom non api inspections for the Doctrine DBAL content graph adapter to make assertions
*/
trait DoctrineDbalProjectionSpyTrait
{
/**
* @Then I expect :number hierarchies to exist in the active write layer
*/
public function iExpectNumberHierarchiesToExistInTheActiveWriteLayer(int $number)
{
$contentGraph = $this->currentContentRepository->getContentGraph($this->currentWorkspaceName);

$layers = $this->contentStreamLayerFinder()->getContentStreamLayers($contentGraph->getContentStreamId());

$count = $this->dbal->fetchOne(<<<SQL
SELECT COUNT(*) FROM {$this->tableNames()->hierarchyRelation()} WHERE contentStreamLayer = :contentStreamLayer
SQL, ['contentStreamLayer' => $layers->getWriteLayer()->value]);

Assert::assertEquals($number, $count);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@

use Behat\Behat\Context\Context as BehatContext;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Doctrine\DBAL\Connection;
use Neos\Behat\FlowBootstrapTrait;
use Neos\ContentGraph\DoctrineDbalAdapter\ContentGraphTableNames;
use Neos\ContentGraph\DoctrineDbalAdapter\Domain\Repository\ContentStreamLayerFinder;
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRBehavioralTestsSubjectProvider;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteRuntimeVariables;
use Neos\ContentRepository\TestSuite\Behavior\Features\Bootstrap\CRTestSuiteTrait;
use Neos\ContentRepository\TestSuite\Fakes\FakeContentDimensionSourceFactory;
use Neos\ContentRepository\TestSuite\Fakes\FakeNodeTypeManagerFactory;
Expand All @@ -31,17 +35,35 @@ class FeatureContext implements BehatContext
{
use FlowBootstrapTrait;
use DoctrineDbalProjectionIntegrityViolatorTrait;
use DoctrineDbalProjectionSpyTrait;
use CRTestSuiteTrait;
use CRTestSuiteRuntimeVariables;
use CRBehavioralTestsSubjectProvider;

protected ContentRepositoryRegistry $contentRepositoryRegistry;

protected Connection $dbal;

public function __construct()
{
self::bootstrapFlow();
$this->contentRepositoryRegistry = $this->getObject(ContentRepositoryRegistry::class);
$this->dbal = $this->getObject(Connection::class);
}

private function tableNames(): ContentGraphTableNames
{
return ContentGraphTableNames::create(
$this->currentContentRepository->id
);
}

$this->setupDbalGraphAdapterIntegrityViolationTrait();
private function contentStreamLayerFinder(): ContentStreamLayerFinder
{
return new ContentStreamLayerFinder(
$this->dbal,
$this->tableNames()
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
@contentrepository
Feature: Run integrity violation detection regarding hierarchy relations and nodes

As a user of the CR I want to know whether there are nodes or hierarchy relations with invalid hashes or parents / children

Background:
Given using the following content dimensions:
| Identifier | Values | Generalizations |
| language | de, gsw, fr | gsw->de |
And using the following node types:
"""yaml
'Neos.ContentRepository.Testing:Document': []
"""
And using identifier "default", I define a content repository
And I am in content repository "default"
And the command CreateRootWorkspace is executed with payload:
| Key | Value |
| workspaceName | "live" |
| newContentStreamId | "cs-identifier" |
And I am in workspace "live" and dimension space point {}
And the command CreateRootNodeAggregateWithNode is executed with payload:
| Key | Value |
| nodeAggregateId | "lady-eleonode-rootford" |
| nodeTypeName | "Neos.ContentRepository:Root" |

Scenario: Detach a hierarchy relation from its parent
When the command CreateWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
| baseWorkspaceName | "live" |
| newContentStreamId | "user-cs-identifier" |

When the command CreateNodeAggregateWithNode is executed with payload:
| Key | Value |
| workspaceName | "live" |
| originDimensionSpacePoint | {"language":"de"} |
| nodeAggregateId | "sir-david-nodenborough" |
| nodeTypeName | "Neos.ContentRepository.Testing:Document" |
| parentNodeAggregateId | "lady-eleonode-rootford" |

# Basically a DeleteWorkspace without the event ContentStreamWasRemoved as we avoid the internal cleanup
When the content stream "user-cs-identifier" was removed without layer cleanup
When the event WorkspaceWasRemoved was published with payload:
| Key | Value |
| workspaceName | "user-test" |

And I run integrity violation detection
Then I expect the integrity violation detection result to contain exactly 1 error
And I expect integrity violation detection result error number 1 to have code 1597909228 and message:
"""
Redundant layer 1 to 2 found for content streams cs-identifier
"""
Loading
Loading