old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -0,0 +1,203 @@
// <copyright file="ConfigCommandTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_014_CLI_config_viewer (CLI-CONFIG-014)
// </copyright>
using StellaOps.Cli.Commands;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
[Trait("Category", TestCategories.Unit)]
public class ConfigCommandTests
{
[Fact]
public void ConfigCatalog_GetAll_ReturnsNonEmptyList()
{
// Act
var entries = ConfigCatalog.GetAll();
// Assert
Assert.NotEmpty(entries);
Assert.True(entries.Count > 50, "Expected at least 50 config entries");
}
[Fact]
public void ConfigCatalog_GetAll_EntriesHaveRequiredProperties()
{
// Act
var entries = ConfigCatalog.GetAll();
// Assert
foreach (var entry in entries)
{
Assert.False(string.IsNullOrWhiteSpace(entry.Path), "Path should not be empty");
Assert.False(string.IsNullOrWhiteSpace(entry.SectionName), "SectionName should not be empty");
Assert.False(string.IsNullOrWhiteSpace(entry.Category), "Category should not be empty");
Assert.False(string.IsNullOrWhiteSpace(entry.Description), "Description should not be empty");
Assert.NotNull(entry.Aliases);
}
}
[Fact]
public void ConfigCatalog_GetAll_PathsAreLowerCase()
{
// Act
var entries = ConfigCatalog.GetAll();
// Assert - paths should be lowercase for determinism
foreach (var entry in entries)
{
Assert.Equal(entry.Path.ToLowerInvariant(), entry.Path);
}
}
[Fact]
public void ConfigCatalog_GetAll_NoDuplicatePaths()
{
// Act
var entries = ConfigCatalog.GetAll();
var paths = entries.Select(e => e.Path).ToList();
// Assert
var duplicates = paths.GroupBy(p => p).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
Assert.Empty(duplicates);
}
[Theory]
[InlineData("policy.determinization")]
[InlineData("pol.det")]
[InlineData("determinization")]
[InlineData("scanner")]
[InlineData("scan")]
[InlineData("notifier")]
[InlineData("notify")]
public void ConfigCatalog_Find_ByPathOrAlias_ReturnsEntry(string pathOrAlias)
{
// Act
var entry = ConfigCatalog.Find(pathOrAlias);
// Assert
Assert.NotNull(entry);
}
[Theory]
[InlineData("POLICY.DETERMINIZATION")]
[InlineData("Policy.Determinization")]
[InlineData("POL.DET")]
public void ConfigCatalog_Find_IsCaseInsensitive(string pathOrAlias)
{
// Act
var entry = ConfigCatalog.Find(pathOrAlias);
// Assert
Assert.NotNull(entry);
Assert.Equal("policy.determinization", entry.Path);
}
[Theory]
[InlineData("policy:determinization")]
[InlineData("policy.determinization")]
public void ConfigCatalog_Find_TreatsColonAndDotAsEquivalent(string pathOrAlias)
{
// Act
var entry = ConfigCatalog.Find(pathOrAlias);
// Assert
Assert.NotNull(entry);
Assert.Equal("policy.determinization", entry.Path);
}
[Theory]
[InlineData("nonexistent")]
[InlineData("foo.bar.baz")]
[InlineData("")]
public void ConfigCatalog_Find_UnknownPath_ReturnsNull(string pathOrAlias)
{
// Act
var entry = ConfigCatalog.Find(pathOrAlias);
// Assert
Assert.Null(entry);
}
[Fact]
public void ConfigCatalog_GetCategories_ReturnsExpectedCategories()
{
// Act
var categories = ConfigCatalog.GetCategories();
// Assert
Assert.Contains("Policy", categories);
Assert.Contains("Scanner", categories);
Assert.Contains("Notifier", categories);
Assert.Contains("Attestor", categories);
}
[Fact]
public void ConfigCatalog_GetCategories_IsSorted()
{
// Act
var categories = ConfigCatalog.GetCategories();
// Assert
var sorted = categories.OrderBy(c => c).ToList();
Assert.Equal(sorted, categories);
}
[Fact]
public void ConfigCatalog_PolicyDeterminization_HasApiEndpoint()
{
// Act
var entry = ConfigCatalog.Find("policy.determinization");
// Assert
Assert.NotNull(entry);
Assert.NotNull(entry.ApiEndpoint);
Assert.Contains("/api/policy/config/determinization", entry.ApiEndpoint);
}
[Fact]
public void ConfigCatalog_Entries_HaveConsistentCategoryNaming()
{
// Act
var entries = ConfigCatalog.GetAll();
var categories = entries.Select(e => e.Category).Distinct().ToList();
// Assert - categories should be PascalCase
foreach (var category in categories)
{
Assert.Matches("^[A-Z][a-zA-Z]*$", category);
}
}
[Fact]
public void ConfigCatalog_AllAliases_AreUnique()
{
// Act
var entries = ConfigCatalog.GetAll();
var allAliases = entries.SelectMany(e => e.Aliases).ToList();
// Assert - aliases should not collide
var duplicates = allAliases
.GroupBy(a => a.ToLowerInvariant())
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
Assert.Empty(duplicates);
}
[Fact]
public void ConfigCatalog_AliasesDontOverlapWithPaths()
{
// Act
var entries = ConfigCatalog.GetAll();
var paths = entries.Select(e => e.Path.ToLowerInvariant()).ToHashSet();
var aliases = entries.SelectMany(e => e.Aliases.Select(a => a.ToLowerInvariant())).ToList();
// Assert - aliases should not match any path (to avoid ambiguity)
var overlaps = aliases.Where(a => paths.Contains(a)).ToList();
Assert.Empty(overlaps);
}
}

View File

@@ -0,0 +1,341 @@
// <copyright file="UnknownsGreyQueueCommandTests.cs" company="StellaOps">
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_010_CLI_unknowns_grey_queue_cli (CLI-UNK-005)
// </copyright>
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Moq.Protected;
using StellaOps.Cli.Commands;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cli.Tests.Commands;
[Trait("Category", TestCategories.Unit)]
public class UnknownsGreyQueueCommandTests
{
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock;
private readonly Mock<HttpMessageHandler> _httpHandlerMock;
private readonly IServiceProvider _services;
public UnknownsGreyQueueCommandTests()
{
_httpHandlerMock = new Mock<HttpMessageHandler>();
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
var httpClient = new HttpClient(_httpHandlerMock.Object)
{
BaseAddress = new Uri("http://localhost:8080")
};
_httpClientFactoryMock
.Setup(f => f.CreateClient("PolicyApi"))
.Returns(httpClient);
var services = new ServiceCollection();
services.AddSingleton(_httpClientFactoryMock.Object);
services.AddSingleton(NullLoggerFactory.Instance);
_services = services.BuildServiceProvider();
}
[Fact]
public void UnknownsSummaryResponse_DeserializesCorrectly()
{
// Arrange
var json = """
{
"hot": 5,
"warm": 10,
"cold": 25,
"resolved": 100,
"total": 140
}
""";
// Act
var response = JsonSerializer.Deserialize<TestUnknownsSummaryResponse>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
// Assert
Assert.NotNull(response);
Assert.Equal(5, response.Hot);
Assert.Equal(10, response.Warm);
Assert.Equal(25, response.Cold);
Assert.Equal(100, response.Resolved);
Assert.Equal(140, response.Total);
}
[Fact]
public void UnknownDto_WithGreyQueueFields_DeserializesCorrectly()
{
// Arrange
var json = """
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"packageId": "pkg:npm/lodash",
"packageVersion": "4.17.21",
"band": "hot",
"score": 85.5,
"uncertaintyFactor": 0.7,
"exploitPressure": 0.9,
"firstSeenAt": "2026-01-10T12:00:00Z",
"lastEvaluatedAt": "2026-01-15T08:00:00Z",
"reasonCode": "Reachability",
"reasonCodeShort": "U-RCH",
"fingerprintId": "sha256:abc123",
"triggers": [
{
"eventType": "epss.updated",
"eventVersion": 1,
"source": "concelier",
"receivedAt": "2026-01-15T07:00:00Z",
"correlationId": "corr-123"
}
],
"nextActions": ["request_vex", "verify_reachability"],
"conflictInfo": {
"hasConflict": true,
"severity": 0.8,
"suggestedPath": "RequireManualReview",
"conflicts": [
{
"signal1": "VEX:not_affected",
"signal2": "Reachability:reachable",
"type": "VexReachabilityContradiction",
"description": "VEX says not affected but reachability shows path",
"severity": 0.8
}
]
},
"observationState": "Disputed"
}
""";
// Act
var unknown = JsonSerializer.Deserialize<TestUnknownDto>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
// Assert
Assert.NotNull(unknown);
Assert.Equal("pkg:npm/lodash", unknown.PackageId);
Assert.Equal("4.17.21", unknown.PackageVersion);
Assert.Equal("hot", unknown.Band);
Assert.Equal(85.5m, unknown.Score);
Assert.Equal("sha256:abc123", unknown.FingerprintId);
Assert.NotNull(unknown.Triggers);
Assert.Single(unknown.Triggers);
Assert.Equal("epss.updated", unknown.Triggers[0].EventType);
Assert.Equal(1, unknown.Triggers[0].EventVersion);
Assert.NotNull(unknown.NextActions);
Assert.Equal(2, unknown.NextActions.Count);
Assert.Contains("request_vex", unknown.NextActions);
Assert.NotNull(unknown.ConflictInfo);
Assert.True(unknown.ConflictInfo.HasConflict);
Assert.Equal(0.8, unknown.ConflictInfo.Severity);
Assert.Equal("RequireManualReview", unknown.ConflictInfo.SuggestedPath);
Assert.Single(unknown.ConflictInfo.Conflicts);
Assert.Equal("Disputed", unknown.ObservationState);
}
[Fact]
public void UnknownProof_HasDeterministicStructure()
{
// Arrange
var proof = new TestUnknownProof
{
Id = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
FingerprintId = "sha256:abc123",
PackageId = "pkg:npm/lodash",
PackageVersion = "4.17.21",
Band = "hot",
Score = 85.5m,
ReasonCode = "Reachability",
Triggers = new List<TestTriggerDto>
{
new() { EventType = "vex.updated", EventVersion = 1, ReceivedAt = DateTimeOffset.Parse("2026-01-15T08:00:00Z") },
new() { EventType = "epss.updated", EventVersion = 1, ReceivedAt = DateTimeOffset.Parse("2026-01-15T07:00:00Z") }
},
EvidenceRefs = new List<TestEvidenceRefDto>
{
new() { Type = "sbom", Uri = "oci://registry/sbom@sha256:def" },
new() { Type = "attestation", Uri = "oci://registry/att@sha256:ghi" }
},
ObservationState = "PendingDeterminization"
};
// Act
var json = JsonSerializer.Serialize(proof, new JsonSerializerOptions { WriteIndented = true });
// Assert
Assert.Contains("\"fingerprintId\"", json.ToLowerInvariant());
Assert.Contains("\"triggers\"", json.ToLowerInvariant());
Assert.Contains("\"evidencerefs\"", json.ToLowerInvariant());
Assert.Contains("\"observationstate\"", json.ToLowerInvariant());
}
[Theory]
[InlineData("accept-risk")]
[InlineData("require-fix")]
[InlineData("defer")]
[InlineData("escalate")]
[InlineData("dispute")]
public void TriageAction_ValidActions_AreRecognized(string action)
{
// Arrange
var validActions = new[] { "accept-risk", "require-fix", "defer", "escalate", "dispute" };
// Act & Assert
Assert.Contains(action, validActions);
}
[Theory]
[InlineData("invalid")]
[InlineData("approve")]
[InlineData("reject")]
[InlineData("")]
public void TriageAction_InvalidActions_AreNotRecognized(string action)
{
// Arrange
var validActions = new[] { "accept-risk", "require-fix", "defer", "escalate", "dispute" };
// Act & Assert
Assert.DoesNotContain(action, validActions);
}
[Fact]
public void TriageRequest_SerializesCorrectly()
{
// Arrange
var request = new TestTriageRequest("accept-risk", "Low priority, mitigated by WAF", 90);
// Act
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
// Assert
Assert.Contains("\"action\":\"accept-risk\"", json);
Assert.Contains("\"reason\":\"Low priority, mitigated by WAF\"", json);
Assert.Contains("\"durationDays\":90", json);
}
[Fact]
public void ExportFormat_CsvEscaping_HandlesSpecialCharacters()
{
// Arrange
var testCases = new[]
{
("simple", "simple"),
("with,comma", "\"with,comma\""),
("with\"quote", "\"with\"\"quote\""),
("with\nnewline", "\"with\nnewline\""),
("normal-value", "normal-value")
};
// Act & Assert
foreach (var (input, expected) in testCases)
{
var result = EscapeCsv(input);
Assert.Equal(expected, result);
}
}
private static string EscapeCsv(string value)
{
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
{
return $"\"{value.Replace("\"", "\"\"")}\"";
}
return value;
}
// Test DTOs matching the CLI internal types
private sealed record TestUnknownsSummaryResponse
{
public int Hot { get; init; }
public int Warm { get; init; }
public int Cold { get; init; }
public int Resolved { get; init; }
public int Total { get; init; }
}
private sealed record TestUnknownDto
{
public Guid Id { get; init; }
public string PackageId { get; init; } = string.Empty;
public string PackageVersion { get; init; } = string.Empty;
public string Band { get; init; } = string.Empty;
public decimal Score { get; init; }
public decimal UncertaintyFactor { get; init; }
public decimal ExploitPressure { get; init; }
public DateTimeOffset FirstSeenAt { get; init; }
public DateTimeOffset LastEvaluatedAt { get; init; }
public string ReasonCode { get; init; } = string.Empty;
public string ReasonCodeShort { get; init; } = string.Empty;
public string? FingerprintId { get; init; }
public IReadOnlyList<TestTriggerDto>? Triggers { get; init; }
public IReadOnlyList<string>? NextActions { get; init; }
public TestConflictInfoDto? ConflictInfo { get; init; }
public string? ObservationState { get; init; }
}
private sealed record TestTriggerDto
{
public string EventType { get; init; } = string.Empty;
public int EventVersion { get; init; }
public string? Source { get; init; }
public DateTimeOffset ReceivedAt { get; init; }
public string? CorrelationId { get; init; }
}
private sealed record TestConflictInfoDto
{
public bool HasConflict { get; init; }
public double Severity { get; init; }
public string SuggestedPath { get; init; } = string.Empty;
public IReadOnlyList<TestConflictDetailDto> Conflicts { get; init; } = [];
}
private sealed record TestConflictDetailDto
{
public string Signal1 { get; init; } = string.Empty;
public string Signal2 { get; init; } = string.Empty;
public string Type { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public double Severity { get; init; }
}
private sealed record TestEvidenceRefDto
{
public string Type { get; init; } = string.Empty;
public string Uri { get; init; } = string.Empty;
public string? Digest { get; init; }
}
private sealed record TestUnknownProof
{
public Guid Id { get; init; }
public string? FingerprintId { get; init; }
public string PackageId { get; init; } = string.Empty;
public string PackageVersion { get; init; } = string.Empty;
public string Band { get; init; } = string.Empty;
public decimal Score { get; init; }
public string ReasonCode { get; init; } = string.Empty;
public IReadOnlyList<TestTriggerDto> Triggers { get; init; } = [];
public IReadOnlyList<TestEvidenceRefDto> EvidenceRefs { get; init; } = [];
public string? ObservationState { get; init; }
public TestConflictInfoDto? ConflictInfo { get; init; }
}
private sealed record TestTriageRequest(string Action, string Reason, int? DurationDays);
}

View File

@@ -0,0 +1,244 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Sprint: SPRINT_20260112_011_CLI_evidence_card_remediate_cli (REMPR-CLI-003)
// Task: REMPR-CLI-003 - CLI tests for open-pr command
using System.CommandLine;
using System.CommandLine.Parsing;
using Xunit;
using StellaOps.TestKit;
using StellaOps.Cli.Extensions;
namespace StellaOps.Cli.Tests;
/// <summary>
/// Tests for the `stella advise open-pr` command argument validation and structure.
/// These tests verify the command structure and argument parsing behavior.
/// </summary>
[Trait("Category", TestCategories.Unit)]
public class OpenPrCommandTests
{
[Fact]
public void OpenPrCommand_ShouldRequirePlanIdArgument()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
// Act
var result = openPrCommand.Parse("");
// Assert
Assert.NotEmpty(result.Errors);
}
[Fact]
public void OpenPrCommand_ShouldAcceptPlanIdArgument()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
// Act
var result = openPrCommand.Parse("plan-abc123");
// Assert - should have no parse errors
Assert.Empty(result.Errors);
}
[Fact]
public void OpenPrCommand_ShouldHaveScmTypeOption()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
// Act - find any option that responds to --scm-type
var result = openPrCommand.Parse("plan-abc123 --scm-type gitlab");
// Assert - should parse without errors
Assert.Empty(result.Errors);
}
[Fact]
public void OpenPrCommand_ShouldDefaultScmTypeToGithub()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
// Act
var result = openPrCommand.Parse("plan-abc123");
var scmType = result.GetValue(scmOption);
// Assert
Assert.Equal("github", scmType);
}
[Fact]
public void OpenPrCommand_ShouldAcceptCustomScmType()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
// Act
var result = openPrCommand.Parse("plan-abc123 --scm-type gitlab");
var scmType = result.GetValue(scmOption);
// Assert
Assert.Equal("gitlab", scmType);
}
[Fact]
public void OpenPrCommand_ShouldAcceptShortScmTypeAlias()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
// Act
var result = openPrCommand.Parse("plan-abc123 -s azure-devops");
var scmType = result.GetValue(scmOption);
// Assert
Assert.Equal("azure-devops", scmType);
}
[Fact]
public void OpenPrCommand_ShouldHaveOutputFormatOption()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
// Act - find any option that responds to --output
var result = openPrCommand.Parse("plan-abc123 --output json");
// Assert - should parse without errors
Assert.Empty(result.Errors);
}
[Fact]
public void OpenPrCommand_ShouldDefaultOutputFormatToTable()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
// Act
var result = openPrCommand.Parse("plan-abc123");
var outputFormat = result.GetValue(outputOption);
// Assert
Assert.Equal("table", outputFormat);
}
[Fact]
public void OpenPrCommand_ShouldAcceptJsonOutputFormat()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
// Act
var result = openPrCommand.Parse("plan-abc123 --output json");
var outputFormat = result.GetValue(outputOption);
// Assert
Assert.Equal("json", outputFormat);
}
[Fact]
public void OpenPrCommand_ShouldAcceptMarkdownOutputFormat()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
// Act
var result = openPrCommand.Parse("plan-abc123 -o markdown");
var outputFormat = result.GetValue(outputOption);
// Assert
Assert.Equal("markdown", outputFormat);
}
[Fact]
public void OpenPrCommand_ShouldHaveVerboseOption()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
// Act
var result = openPrCommand.Parse("plan-abc123 --verbose");
// Assert
Assert.Empty(result.Errors);
}
[Fact]
public void OpenPrCommand_ShouldParseAllOptionsCorrectly()
{
// Arrange
var openPrCommand = BuildOpenPrCommand();
// Act
var result = openPrCommand.Parse("plan-test-789 --scm-type azure-devops --output json --verbose");
// Assert
Assert.Empty(result.Errors);
var planIdArg = openPrCommand.Arguments.OfType<Argument<string>>().First(a => a.Name == "plan-id");
Assert.NotNull(planIdArg);
Assert.Equal("plan-test-789", result.GetValue(planIdArg));
var scmOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--scm-type"));
Assert.NotNull(scmOption);
Assert.Equal("azure-devops", result.GetValue(scmOption));
var outputOption = openPrCommand.Options.OfType<Option<string>>().First(o => o.Aliases.Contains("--output"));
Assert.NotNull(outputOption);
Assert.Equal("json", result.GetValue(outputOption));
var verboseOption = openPrCommand.Options.OfType<Option<bool>>().First(o => o.Aliases.Contains("--verbose"));
Assert.NotNull(verboseOption);
Assert.True(result.GetValue(verboseOption));
}
/// <summary>
/// Build the open-pr command structure for testing.
/// This mirrors the structure in CommandFactory.BuildOpenPrCommand.
/// Note: Defaults are verified through the actual parsing behavior, not Option properties.
/// </summary>
private static Command BuildOpenPrCommand()
{
var planIdArg = new Argument<string>("plan-id")
{
Description = "Remediation plan ID to apply"
};
// Use correct System.CommandLine 2.x constructors
var scmTypeOption = new Option<string>("--scm-type", new[] { "-s" })
{
Description = "SCM type (github, gitlab, azure-devops, gitea)"
};
scmTypeOption.SetDefaultValue("github");
var outputOption = new Option<string>("--output", new[] { "-o" })
{
Description = "Output format: table (default), json, markdown"
};
outputOption.SetDefaultValue("table");
var verboseOption = new Option<bool>("--verbose", new[] { "-v" })
{
Description = "Enable verbose output"
};
var openPrCommand = new Command("open-pr", "Apply a remediation plan by creating a PR/MR in the target SCM")
{
planIdArg,
scmTypeOption,
outputOption,
verboseOption
};
return openPrCommand;
}
}

View File

@@ -17,6 +17,7 @@
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Moq" />
<PackageReference Include="Spectre.Console.Testing" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="coverlet.collector" >
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>