Competitor Ingest Anomaly Regression Tests (CM4)

Status: Draft · Date: 2025-12-04 Scope: Define anomaly regression test suite for ingest pipeline covering schema drift, nullables, encoding, and ordering anomalies.

Objectives

Test Categories

1. Schema Drift Tests

Detect when upstream tools change their output schema.

tests/anomaly/schema-drift/
├── syft/
│   ├── v1.0.0-baseline.json        # Known good output
│   ├── v1.5.0-new-fields.json      # Added fields
│   ├── v1.5.0-removed-fields.json  # Removed fields
│   ├── v1.5.0-type-change.json     # Field type changed
│   └── expected-results.json
├── trivy/
│   └── ... (same structure)
└── clair/
    └── ... (same structure)

Test Cases

TestInputExpected Behavior
new_optional_fieldOutput with new fieldAccept, ignore new field
new_required_fieldOutput with new required fieldWarn, map if possible
removed_optional_fieldOutput missing optional fieldAccept
removed_required_fieldOutput missing required fieldReject
field_type_changeField type differs from schemaReject or coerce
field_renameField renamed without mappingWarn, check mapping

Schema Drift Fixture

{
  "test": "new_optional_field",
  "tool": "syft",
  "inputVersion": "1.5.0",
  "baselineVersion": "1.0.0",
  "input": {
    "artifacts": [
      {
        "name": "lib-a",
        "version": "1.0.0",
        "purl": "pkg:npm/lib-a@1.0.0",
        "newField": "unexpected value"
      }
    ]
  },
  "expected": {
    "status": "accepted",
    "warnings": ["unknown_field:newField"],
    "normalizedHash": "b3:..."
  }
}

2. Nullable/Missing Field Tests

Validate handling of null, empty, and missing values.

tests/anomaly/nullables/
├── null-values.json
├── empty-strings.json
├── empty-arrays.json
├── missing-optional.json
├── missing-required.json
└── expected-results.json

Test Cases

TestInputExpected Behavior
null_optionalOptional field is nullAccept, omit from output
null_requiredRequired field is nullReject
empty_stringString field is “”Accept, preserve or omit
empty_arrayArray field is []Accept, preserve
missing_optionalOptional field absentAccept
missing_requiredRequired field absentReject

Nullable Fixture

{
  "test": "null_optional",
  "tool": "syft",
  "input": {
    "artifacts": [
      {
        "name": "lib-a",
        "version": "1.0.0",
        "purl": "pkg:npm/lib-a@1.0.0",
        "licenses": null
      }
    ]
  },
  "expected": {
    "status": "accepted",
    "output": {
      "components": [
        {
          "name": "lib-a",
          "version": "1.0.0",
          "purl": "pkg:npm/lib-a@1.0.0"
        }
      ]
    },
    "normalizedHash": "b3:..."
  }
}

3. Encoding Tests

Validate proper handling of character encoding and escaping.

tests/anomaly/encoding/
├── utf8-valid.json
├── utf8-bom.json
├── latin1-fallback.json
├── unicode-escapes.json
├── special-chars.json
├── json-escaping.json
└── expected-results.json

Test Cases

TestInputExpected Behavior
utf8_validStandard UTF-8Accept
utf8_bomUTF-8 with BOMAccept, strip BOM
unicode_escapes\u0041 style escapesAccept, decode
special_charsTabs, newlines in stringsAccept, preserve or escape
control_charsControl characters (0x00-0x1F)Reject or sanitize
surrogate_pairsEmoji and supplementary charsAccept

Encoding Fixture

{
  "test": "special_chars",
  "tool": "syft",
  "input": {
    "artifacts": [
      {
        "name": "lib-with-tab\ttab",
        "version": "1.0.0",
        "description": "Line1\nLine2"
      }
    ]
  },
  "expected": {
    "status": "accepted",
    "output": {
      "components": [
        {
          "name": "lib-with-tab\ttab",
          "version": "1.0.0"
        }
      ]
    },
    "normalizedHash": "b3:..."
  }
}

4. Ordering Tests

Verify deterministic ordering is maintained across inputs.

tests/anomaly/ordering/
├── unsorted-components.json
├── reversed-components.json
├── random-order.json
├── unicode-sort.json
├── case-sensitivity.json
└── expected-results.json

Test Cases

TestInputExpected Behavior
unsorted_inputComponents in random orderSort deterministically
reversed_inputComponents in reverse orderSort deterministically
same_after_sortPre-sorted inputSame output as unsorted
unicode_sortUnicode component namesLocale-invariant sort
case_sensitivityMixed case namesCase-insensitive sort

Ordering Fixture

{
  "test": "unsorted_input",
  "tool": "syft",
  "input": {
    "artifacts": [
      {"name": "zebra", "version": "1.0.0", "purl": "pkg:npm/zebra@1.0.0"},
      {"name": "apple", "version": "1.0.0", "purl": "pkg:npm/apple@1.0.0"},
      {"name": "mango", "version": "1.0.0", "purl": "pkg:npm/mango@1.0.0"}
    ]
  },
  "expected": {
    "status": "accepted",
    "output": {
      "components": [
        {"name": "apple", "version": "1.0.0", "purl": "pkg:npm/apple@1.0.0"},
        {"name": "mango", "version": "1.0.0", "purl": "pkg:npm/mango@1.0.0"},
        {"name": "zebra", "version": "1.0.0", "purl": "pkg:npm/zebra@1.0.0"}
      ]
    },
    "normalizedHash": "b3:..."
  }
}

Golden Fixtures

Hash File Format

# tests/anomaly/hashes.txt
schema-drift/syft/v1.0.0-baseline.json: BLAKE3=... SHA256=...
schema-drift/syft/expected-results.json: BLAKE3=... SHA256=...
nullables/null-values.json: BLAKE3=... SHA256=...
nullables/expected-results.json: BLAKE3=... SHA256=...
encoding/utf8-valid.json: BLAKE3=... SHA256=...
encoding/expected-results.json: BLAKE3=... SHA256=...
ordering/unsorted-components.json: BLAKE3=... SHA256=...
ordering/expected-results.json: BLAKE3=... SHA256=...

CI Integration

Test Workflow

# .gitea/workflows/anomaly-tests.yml
name: Anomaly Regression Tests

on:
  push:
    paths:
      - 'src/Scanner/Adapters/**'
      - 'tests/anomaly/**'
  pull_request:

jobs:
  anomaly-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Verify fixture hashes
        run: scripts/scanner/verify-anomaly-fixtures.sh

      - name: Run schema drift tests
        run: |
          dotnet test src/Scanner/__Tests/StellaOps.Scanner.Anomaly.Tests \
            --filter "Category=SchemaDrift"

      - name: Run nullable tests
        run: |
          dotnet test src/Scanner/__Tests/StellaOps.Scanner.Anomaly.Tests \
            --filter "Category=Nullable"

      - name: Run encoding tests
        run: |
          dotnet test src/Scanner/__Tests/StellaOps.Scanner.Anomaly.Tests \
            --filter "Category=Encoding"

      - name: Run ordering tests
        run: |
          dotnet test src/Scanner/__Tests/StellaOps.Scanner.Anomaly.Tests \
            --filter "Category=Ordering"

Test Runner

// src/Scanner/__Tests/StellaOps.Scanner.Anomaly.Tests/AnomalyTestRunner.cs
[Category("SchemaDrift")]
[Theory]
[MemberData(nameof(GetSchemaDriftTestCases))]
public async Task SchemaDrift_HandledCorrectly(AnomalyTestCase testCase)
{
    // Arrange
    var adapter = _adapterFactory.Create(testCase.Tool);

    // Act
    var result = await adapter.NormalizeAsync(testCase.Input);

    // Assert
    Assert.Equal(testCase.Expected.Status, result.Status);
    Assert.Equal(testCase.Expected.Warnings, result.Warnings);

    if (testCase.Expected.NormalizedHash != null)
    {
        var hash = Blake3.HashData(Encoding.UTF8.GetBytes(
            JsonSerializer.Serialize(result.Output)));
        Assert.Equal(testCase.Expected.NormalizedHash,
            $"b3:{Convert.ToHexString(hash).ToLowerInvariant()}");
    }
}

Failure Handling

On Test Failure

  1. Schema Drift: Create issue, update adapter mapping
  2. Nullable Handling: Fix normalization logic
  3. Encoding Error: Fix encoding detection/conversion
  4. Ordering Violation: Fix sort comparator

Failure Report

{
  "failure": {
    "category": "schema_drift",
    "test": "new_required_field",
    "tool": "syft",
    "input": {...},
    "expected": {...},
    "actual": {...},
    "diff": [
      {"path": "/status", "expected": "accepted", "actual": "rejected"}
    ],
    "timestamp": "2025-12-04T12:00:00Z"
  }
}