up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-24 09:07:40 +02:00
parent 150b3730ef
commit e6119cbe91
59 changed files with 1827 additions and 204 deletions

View File

@@ -779,6 +779,124 @@ public sealed class CommandHandlersTests
}
}
[Fact]
public async Task HandleAdviseBatchAsync_RunsAllAdvisories()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = "Summary",
CacheKey = "batch-plan",
PromptTemplate = "prompts/advisory/summary.liquid",
Budget = new AdvisoryTaskBudgetModel { PromptTokens = 64, CompletionTokens = 32 },
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var outputs = new Queue<AdvisoryPipelineOutputModel?>(new[]
{
new AdvisoryPipelineOutputModel
{
CacheKey = "k1",
TaskType = "Summary",
Profile = "default",
Prompt = "P1",
Response = "Body one",
Citations = new[] { new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-1", ChunkId = "c-1" } },
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = false,
SanitizedPrompt = "P1",
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:1",
OutputHash = "sha256:1out",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
PlanFromCache = false
},
new AdvisoryPipelineOutputModel
{
CacheKey = "k2",
TaskType = "Summary",
Profile = "default",
Prompt = "P2",
Response = "Body two",
Citations = new[] { new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-2", ChunkId = "c-2" } },
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = false,
SanitizedPrompt = "P2",
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:2",
OutputHash = "sha256:2out",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
PlanFromCache = false
}
});
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputQueue = outputs
};
var provider = BuildServiceProvider(backend);
using var tempDir = new TempDirectory();
await CommandHandlers.HandleAdviseBatchAsync(
provider,
AdvisoryAiTaskType.Summary,
new[] { "ADV-1", "ADV-2" },
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: false,
timeoutSeconds: 0,
outputFormat: AdvisoryOutputFormat.Markdown,
outputDirectory: tempDir.Path,
verbose: false,
cancellationToken: CancellationToken.None);
var file1 = Path.Combine(tempDir.Path, "ADV-1-summary.md");
var file2 = Path.Combine(tempDir.Path, "ADV-2-summary.md");
Assert.True(File.Exists(file1));
Assert.True(File.Exists(file2));
Assert.Contains("Body one", await File.ReadAllTextAsync(file1));
Assert.Contains("Body two", await File.ReadAllTextAsync(file2));
Assert.Equal(0, Environment.ExitCode);
Assert.Contains("Advisory Batch", testConsole.Output, StringComparison.OrdinalIgnoreCase);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations()
{
@@ -976,7 +1094,198 @@ public sealed class CommandHandlersTests
}
[Fact]
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForRemediation()
public async Task HandleAdviseRunAsync_ReturnsGuardrailExitCodeOnBlock()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Remediation.ToString(),
CacheKey = "cache-guard",
PromptTemplate = "prompts/advisory/remediation.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 256,
CompletionTokens = 64
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Blocked output",
Citations = Array.Empty<AdvisoryOutputCitationModel>(),
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = true,
SanitizedPrompt = "Blocked output",
Violations = new[]
{
new AdvisoryOutputGuardrailViolationModel
{
Code = "PROMPT_INJECTION",
Message = "Detected prompt injection attempt."
}
},
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:ccc",
OutputHash = "sha256:ddd",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T13:05:00Z", CultureInfo.InvariantCulture),
PlanFromCache = true
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Remediation,
"ADV-2",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: true,
timeoutSeconds: 0,
outputFormat: AdvisoryOutputFormat.Table,
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(65, Environment.ExitCode);
Assert.Contains("Guardrail Violations", testConsole.Output, StringComparison.OrdinalIgnoreCase);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForExplain()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = "Conflict",
CacheKey = "plan-conflict",
PromptTemplate = "prompts/advisory/conflict.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 128,
CompletionTokens = 64
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Sanitized prompt",
Response = "Rendered conflict body.",
Citations = new[]
{
new AdvisoryOutputCitationModel { Index = 1, DocumentId = "doc-42", ChunkId = "chunk-42" }
},
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = false,
SanitizedPrompt = "Sanitized prompt",
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:conflict-in",
OutputHash = "sha256:conflict-out",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
PlanFromCache = false
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
var outputPath = Path.GetTempFileName();
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Conflict,
"ADV-42",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: false,
timeoutSeconds: 0,
outputFormat: AdvisoryOutputFormat.Markdown,
outputPath: outputPath,
verbose: false,
cancellationToken: CancellationToken.None);
var markdown = await File.ReadAllTextAsync(outputPath);
Assert.Contains("Conflict", markdown, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Rendered conflict body", markdown, StringComparison.OrdinalIgnoreCase);
Assert.Contains("doc-42", markdown, StringComparison.OrdinalIgnoreCase);
Assert.Contains("chunk-42", markdown, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Citations", markdown, StringComparison.OrdinalIgnoreCase);
Assert.Equal(0, Environment.ExitCode);
Assert.Contains("Conflict", testConsole.Output, StringComparison.OrdinalIgnoreCase);
Assert.Equal(AdvisoryAiTaskType.Conflict, backend.AdvisoryPlanRequests.Last().TaskType);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_WritesMarkdownWithCitations_ForRemediationTask()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
@@ -1073,99 +1382,6 @@ public sealed class CommandHandlersTests
}
}
[Fact]
public async Task HandleAdviseRunAsync_ReturnsGuardrailExitCodeOnBlock()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Remediation.ToString(),
CacheKey = "cache-guard",
PromptTemplate = "prompts/advisory/remediation.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 256,
CompletionTokens = 64
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Blocked output",
Citations = Array.Empty<AdvisoryOutputCitationModel>(),
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = true,
SanitizedPrompt = "Blocked output",
Violations = new[]
{
new AdvisoryOutputGuardrailViolationModel
{
Code = "PROMPT_INJECTION",
Message = "Detected prompt injection attempt."
}
},
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:ccc",
OutputHash = "sha256:ddd",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T13:05:00Z", CultureInfo.InvariantCulture),
PlanFromCache = true
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Remediation,
"ADV-2",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: true,
timeoutSeconds: 0,
outputFormat: AdvisoryOutputFormat.Table,
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(65, Environment.ExitCode);
Assert.Contains("Guardrail Violations", testConsole.Output, StringComparison.OrdinalIgnoreCase);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_TimesOutWhenOutputMissing()
{