Initial commit to new repository
26
src/AxCopilot.Tests/AxCopilot.Tests.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>AxCopilot.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.9.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AxCopilot\AxCopilot.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
279
src/AxCopilot.Tests/Core/FuzzyEngineTests.cs
Normal file
@@ -0,0 +1,279 @@
|
||||
using FluentAssertions;
|
||||
using AxCopilot.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Core;
|
||||
|
||||
public class FuzzyEngineTests
|
||||
{
|
||||
// ─── CalculateScore 기본 매칭 ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_ExactMatch_ReturnsHighestScore()
|
||||
{
|
||||
var score = FuzzyEngine.CalculateScore("notepad", "notepad", 0);
|
||||
score.Should().BeGreaterThanOrEqualTo(1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_PrefixMatch_ReturnsHighScore()
|
||||
{
|
||||
var score = FuzzyEngine.CalculateScore("note", "notepad", 0);
|
||||
score.Should().BeGreaterThanOrEqualTo(800);
|
||||
score.Should().BeLessThan(1000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_ContainsMatch_ReturnsMediumScore()
|
||||
{
|
||||
var score = FuzzyEngine.CalculateScore("pad", "notepad", 0);
|
||||
score.Should().BeGreaterThanOrEqualTo(600);
|
||||
score.Should().BeLessThan(800);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_NoMatch_ReturnsZero()
|
||||
{
|
||||
var score = FuzzyEngine.CalculateScore("xyz", "notepad", 0);
|
||||
score.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_EmptyQuery_ReturnsZero()
|
||||
{
|
||||
var score = FuzzyEngine.CalculateScore("", "notepad", 0);
|
||||
score.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_BaseScore_AddsToResult()
|
||||
{
|
||||
var scoreWithBase = FuzzyEngine.CalculateScore("notepad", "notepad", 100);
|
||||
var scoreWithout = FuzzyEngine.CalculateScore("notepad", "notepad", 0);
|
||||
scoreWithBase.Should().Be(scoreWithout + 100);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_ExactBeforePrefix()
|
||||
{
|
||||
var exact = FuzzyEngine.CalculateScore("note", "note", 0);
|
||||
var prefix = FuzzyEngine.CalculateScore("not", "note", 0);
|
||||
exact.Should().BeGreaterThan(prefix);
|
||||
}
|
||||
|
||||
// ─── FuzzyMatch ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void FuzzyMatch_AllCharsPresent_ReturnsPositive()
|
||||
{
|
||||
var score = FuzzyEngine.FuzzyMatch("ntpd", "notepad");
|
||||
score.Should().BePositive();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FuzzyMatch_CharsMissing_ReturnsZero()
|
||||
{
|
||||
var score = FuzzyEngine.FuzzyMatch("xyz", "notepad");
|
||||
score.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FuzzyMatch_ConsecutiveCharsScoreHigher()
|
||||
{
|
||||
var consecutive = FuzzyEngine.FuzzyMatch("not", "notepad");
|
||||
var scattered = FuzzyEngine.FuzzyMatch("ntp", "notepad");
|
||||
consecutive.Should().BeGreaterThan(scattered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FuzzyMatch_EmptyQuery_ReturnsPositive()
|
||||
{
|
||||
var score = FuzzyEngine.FuzzyMatch("", "notepad");
|
||||
score.Should().BeGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FuzzyMatch_FullMatch_ReturnsHighScore()
|
||||
{
|
||||
var full = FuzzyEngine.FuzzyMatch("abcde", "abcde");
|
||||
var partial = FuzzyEngine.FuzzyMatch("ace", "abcde");
|
||||
full.Should().BeGreaterThan(partial);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FuzzyMatch_MinimumScoreGuaranteed()
|
||||
{
|
||||
var score = FuzzyEngine.FuzzyMatch("ntpd", "notepad");
|
||||
score.Should().BeGreaterThanOrEqualTo(50);
|
||||
}
|
||||
|
||||
// ─── 한글 자모 분리 ─────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("가", "ㄱㅏ")]
|
||||
[InlineData("한", "ㅎㅏㄴ")]
|
||||
[InlineData("글", "ㄱㅡㄹ")]
|
||||
[InlineData("abc", "abc")]
|
||||
[InlineData("가a나", "ㄱㅏaㄴㅏ")]
|
||||
public void DecomposeToJamo_ReturnsCorrectJamo(string input, string expected)
|
||||
{
|
||||
FuzzyEngine.DecomposeToJamo(input).Should().Be(expected);
|
||||
}
|
||||
|
||||
// ─── 자모 기반 포함 검색 ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void JamoContainsScore_MiddleWord_ReturnsPositive()
|
||||
{
|
||||
// "모장" → "메모장" (자모 분리 후 연속 매칭)
|
||||
var score = FuzzyEngine.JamoContainsScore("메모장", "모장");
|
||||
score.Should().BePositive();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JamoContainsScore_NoMatch_ReturnsZero()
|
||||
{
|
||||
var score = FuzzyEngine.JamoContainsScore("메모장", "가나");
|
||||
score.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JamoContainsScore_SubsequenceMatch_ReturnsPositive()
|
||||
{
|
||||
// "메장" → 메-모-장에서 비연속 자모 매칭
|
||||
var score = FuzzyEngine.JamoContainsScore("메모장", "메장");
|
||||
score.Should().BePositive();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JamoContainsScore_NonKorean_ReturnsZero()
|
||||
{
|
||||
var score = FuzzyEngine.JamoContainsScore("notepad", "pad");
|
||||
score.Should().Be(0); // 영어는 Contains에서 이미 처리
|
||||
}
|
||||
|
||||
// ─── 한글 초성 검색 ──────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("ㄴ", true)]
|
||||
[InlineData("ㄴㅌ", true)]
|
||||
[InlineData("ㄱㄴㄷ", true)]
|
||||
public void IsChosung_ValidChosung_ReturnsTrue(string text, bool expected)
|
||||
{
|
||||
FuzzyEngine.IsChosung(text).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("notepad", false)]
|
||||
[InlineData("노트패드", false)]
|
||||
[InlineData("a", false)]
|
||||
[InlineData("ㄴa", false)]
|
||||
public void IsChosung_NonChosung_ReturnsFalse(string text, bool expected)
|
||||
{
|
||||
FuzzyEngine.IsChosung(text).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChosung_HangulChar_ReturnsCorrectChosung()
|
||||
{
|
||||
FuzzyEngine.GetChosung('나').Should().Be('ㄴ');
|
||||
FuzzyEngine.GetChosung('가').Should().Be('ㄱ');
|
||||
FuzzyEngine.GetChosung('하').Should().Be('ㅎ');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetChosung_NonHangul_ReturnsNull()
|
||||
{
|
||||
FuzzyEngine.GetChosung('a').Should().Be('\0');
|
||||
FuzzyEngine.GetChosung('1').Should().Be('\0');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsChosung_ConsecutiveMatch_ReturnsTrue()
|
||||
{
|
||||
FuzzyEngine.ContainsChosung("노트패드", "ㄴㅌ").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsChosung_NonMatchingChosung_ReturnsFalse()
|
||||
{
|
||||
FuzzyEngine.ContainsChosung("노트패드", "ㅅ").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsChosung_PartialMatch_ReturnsTrue()
|
||||
{
|
||||
FuzzyEngine.ContainsChosung("계산기", "ㄱㅅ").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsChosung_QueryLongerThanTarget_ReturnsFalse()
|
||||
{
|
||||
FuzzyEngine.ContainsChosung("가", "ㄱㄴㄷㄹ").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContainsChosung_NonConsecutive_ReturnsTrue()
|
||||
{
|
||||
// "ㅁㅊ" → 메모장(ㅁㅁㅈ) — 안 맞음 (ㅊ가 없으므로)
|
||||
FuzzyEngine.ContainsChosung("메모장", "ㅁㅊ").Should().BeFalse();
|
||||
|
||||
// "ㅁㅈ" → 메모장(ㅁㅁㅈ) — 비연속 매칭
|
||||
FuzzyEngine.ContainsChosung("메모장", "ㅁㅈ").Should().BeTrue();
|
||||
}
|
||||
|
||||
// ─── 초성 점수 매칭 ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ChosungMatchScore_PureChosung_Consecutive()
|
||||
{
|
||||
var score = FuzzyEngine.ChosungMatchScore("계산기", "ㄱㅅ");
|
||||
score.Should().BeGreaterThanOrEqualTo(500);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChosungMatchScore_PureChosung_Subsequence()
|
||||
{
|
||||
// "ㅁㅈ" → 메모장 (ㅁ...ㅈ 비연속)
|
||||
var score = FuzzyEngine.ChosungMatchScore("메모장", "ㅁㅈ");
|
||||
score.Should().BePositive();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChosungMatchScore_MixedQuery()
|
||||
{
|
||||
// "ㅁ장" → 혼합: ㅁ은 초성, 장은 완성형
|
||||
var score = FuzzyEngine.ChosungMatchScore("메모장", "ㅁ장");
|
||||
score.Should().BePositive();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChosungMatchScore_NoMatch_ReturnsZero()
|
||||
{
|
||||
var score = FuzzyEngine.ChosungMatchScore("메모장", "ㅋㅋ");
|
||||
score.Should().Be(0);
|
||||
}
|
||||
|
||||
// ─── 통합 점수 우선순위 ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_ScoreHierarchy()
|
||||
{
|
||||
// 정확 > 시작 > 포함 > 자모포함 > 초성 > fuzzy
|
||||
var exact = FuzzyEngine.CalculateScore("메모장", "메모장", 0);
|
||||
var prefix = FuzzyEngine.CalculateScore("메모", "메모장", 0);
|
||||
var contains = FuzzyEngine.CalculateScore("모장", "메모장", 0);
|
||||
|
||||
exact.Should().BeGreaterThan(prefix);
|
||||
prefix.Should().BeGreaterThan(contains);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateScore_JamoBeforeChosung()
|
||||
{
|
||||
// "모장" (자모 포함) > "ㅁㅈ" (초성 비연속)
|
||||
var jamo = FuzzyEngine.CalculateScore("모장", "메모장", 0);
|
||||
var chosung = FuzzyEngine.CalculateScore("ㅁㅈ", "메모장", 0);
|
||||
jamo.Should().BeGreaterThan(chosung);
|
||||
}
|
||||
}
|
||||
215
src/AxCopilot.Tests/Handlers/ClipboardTransformTests.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System.Text;
|
||||
using FluentAssertions;
|
||||
using AxCopilot.Handlers;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Handlers;
|
||||
|
||||
public class ClipboardTransformTests
|
||||
{
|
||||
// ─── 대소문자 변환 ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Upper_ConvertsToUppercase()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$upper", "hello world").Should().Be("HELLO WORLD");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lower_ConvertsToLowercase()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$lower", "HELLO WORLD").Should().Be("hello world");
|
||||
}
|
||||
|
||||
// ─── Base64 ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Base64Encode_EncodesCorrectly()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$b64e", "hello").Should().Be("aGVsbG8=");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Base64Decode_DecodesCorrectly()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$b64d", "aGVsbG8=").Should().Be("hello");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Base64_RoundTrip()
|
||||
{
|
||||
var original = "AX Copilot 테스트";
|
||||
var encoded = ClipboardHandler.ExecuteBuiltin("$b64e", original)!;
|
||||
var decoded = ClipboardHandler.ExecuteBuiltin("$b64d", encoded);
|
||||
decoded.Should().Be(original);
|
||||
}
|
||||
|
||||
// ─── URL 인코딩 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UrlEncode_EncodesSpaces()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$urle", "hello world").Should().Be("hello%20world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UrlDecode_DecodesSpaces()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$urld", "hello%20world").Should().Be("hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UrlEncode_EncodesSpecialChars()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$urle", "a=b&c=d");
|
||||
result.Should().Be("a%3Db%26c%3Dd");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UrlDecode_DecodesSpecialChars()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$urld", "a%3Db%26c%3Dd");
|
||||
result.Should().Be("a=b&c=d");
|
||||
}
|
||||
|
||||
// ─── 문자열 처리 ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Trim_RemovesLeadingTrailingWhitespace()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$trim", " hello ").Should().Be("hello");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trim_PreservesInternalSpaces()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$trim", " hello world ").Should().Be("hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lines_RemovesEmptyLines()
|
||||
{
|
||||
var input = "a\n\nb\n \nc";
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$lines", input);
|
||||
result.Should().NotContain("\n\n");
|
||||
result.Should().Contain("a");
|
||||
result.Should().Contain("b");
|
||||
result.Should().Contain("c");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lines_TrimsEachLine()
|
||||
{
|
||||
var input = " hello \n world ";
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$lines", input)!;
|
||||
result.Should().Contain("hello");
|
||||
result.Should().Contain("world");
|
||||
result.Should().NotContain(" hello");
|
||||
}
|
||||
|
||||
// ─── 타임스탬프 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Timestamp_EpochZero_Returns1970()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$ts", "0");
|
||||
result.Should().NotBeNullOrEmpty();
|
||||
result.Should().Contain("1970");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Timestamp_ValidEpoch_ReturnsDateString()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$ts", "1700000000");
|
||||
result.Should().NotBeNullOrEmpty();
|
||||
result.Should().MatchRegex(@"\d{4}-\d{2}-\d{2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Timestamp_InvalidInput_ReturnsNull()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$ts", "not-a-number").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Epoch_ValidDate_ReturnsNumber()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$epoch", "1970-01-01");
|
||||
result.Should().NotBeNullOrEmpty();
|
||||
long.TryParse(result, out _).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Epoch_InvalidDate_ReturnsNull()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$epoch", "not-a-date").Should().BeNull();
|
||||
}
|
||||
|
||||
// ─── JSON 포맷팅 ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void JsonFormat_MinifiedJson_AddsIndentation()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$json", "{\"a\":1,\"b\":2}");
|
||||
result.Should().NotBeNullOrEmpty();
|
||||
result.Should().Contain("\n");
|
||||
result.Should().Contain("\"a\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonFormat_AlreadyFormatted_StaysValid()
|
||||
{
|
||||
var formatted = "{\n \"a\": 1\n}";
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$json", formatted);
|
||||
result.Should().NotBeNullOrEmpty();
|
||||
result.Should().Contain("\"a\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JsonFormat_InvalidJson_ReturnsOriginal()
|
||||
{
|
||||
var invalid = "not json";
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$json", invalid);
|
||||
result.Should().Be(invalid); // 파싱 실패 시 원본 반환
|
||||
}
|
||||
|
||||
// ─── 마크다운 제거 ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void StripMarkdown_RemovesBold()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$md", "**bold text**");
|
||||
result.Should().NotContain("**");
|
||||
result.Should().Contain("bold text");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripMarkdown_RemovesItalic()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$md", "*italic*");
|
||||
result.Should().NotContain("*italic*");
|
||||
result.Should().Contain("italic");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripMarkdown_RemovesInlineCode()
|
||||
{
|
||||
var result = ClipboardHandler.ExecuteBuiltin("$md", "`code`");
|
||||
result.Should().NotContain("`");
|
||||
result.Should().Contain("code");
|
||||
}
|
||||
|
||||
// ─── 알 수 없는 키 ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UnknownKey_ReturnsNull()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("$unknown", "input").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyKey_ReturnsNull()
|
||||
{
|
||||
ClipboardHandler.ExecuteBuiltin("", "input").Should().BeNull();
|
||||
}
|
||||
}
|
||||
92
src/AxCopilot.Tests/Services/AgentHookRunnerTests.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class AgentHookRunnerTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryParseStructuredPayload_ParsesExtendedPermissionAndContextShapes()
|
||||
{
|
||||
const string payload =
|
||||
"""
|
||||
{"updatedInput":{"path":"src/app.cs"},"updatedPermissions":{"file_write":"auto","http_tool":{"permission":"deny"}},"additionalContext":["line1","line2"],"message":"ok"}
|
||||
""";
|
||||
|
||||
var parsed = InvokeTryParseStructuredPayload(
|
||||
payload,
|
||||
out var updatedInput,
|
||||
out var updatedPermissions,
|
||||
out var additionalContext,
|
||||
out var message);
|
||||
|
||||
parsed.Should().BeTrue();
|
||||
updatedInput.HasValue.Should().BeTrue();
|
||||
updatedInput!.Value.TryGetProperty("path", out var pathProp).Should().BeTrue();
|
||||
pathProp.GetString().Should().Be("src/app.cs");
|
||||
updatedPermissions.Should().NotBeNull();
|
||||
updatedPermissions!["file_write"].Should().Be("auto");
|
||||
updatedPermissions["http_tool"].Should().Be("deny");
|
||||
additionalContext.Should().Be("line1\nline2");
|
||||
message.Should().Be("ok");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseStructuredPayload_ParsesPermissionUpdatesArrayAlias()
|
||||
{
|
||||
const string payload =
|
||||
"""
|
||||
{"permissionUpdates":[{"tool":"file_edit","permission":"ask"},{"tool":"*","permission":"deny"}]}
|
||||
""";
|
||||
|
||||
var parsed = InvokeTryParseStructuredPayload(
|
||||
payload,
|
||||
out _,
|
||||
out var updatedPermissions,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
parsed.Should().BeTrue();
|
||||
updatedPermissions.Should().NotBeNull();
|
||||
updatedPermissions!["file_edit"].Should().Be("ask");
|
||||
updatedPermissions["*"].Should().Be("deny");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseStructuredPayload_ReturnsFalseForNonJsonOutput()
|
||||
{
|
||||
var parsed = InvokeTryParseStructuredPayload(
|
||||
"plain output without json",
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
parsed.Should().BeFalse();
|
||||
}
|
||||
|
||||
private static bool InvokeTryParseStructuredPayload(
|
||||
string rawOutput,
|
||||
out JsonElement? updatedInput,
|
||||
out Dictionary<string, string>? updatedPermissions,
|
||||
out string? additionalContext,
|
||||
out string? message)
|
||||
{
|
||||
var method = typeof(AgentHookRunner).GetMethod(
|
||||
"TryParseStructuredPayload",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var args = new object?[] { rawOutput, null, null, null, null };
|
||||
var result = (bool)method!.Invoke(null, args)!;
|
||||
|
||||
updatedInput = (JsonElement?)args[1];
|
||||
updatedPermissions = (Dictionary<string, string>?)args[2];
|
||||
additionalContext = (string?)args[3];
|
||||
message = (string?)args[4];
|
||||
return result;
|
||||
}
|
||||
}
|
||||
2060
src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs
Normal file
456
src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
[Trait("Suite", "ParityBenchmark")]
|
||||
public class AgentLoopE2ETests
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunAsync_ExecutesToolCall_AndCompletesWithFinalText()
|
||||
{
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildToolCallResponse("math_eval", new { expression = "1+2" }, "계산 중"),
|
||||
BuildTextResponse("최종 답변: 3"),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "1+2 계산해줘" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("3");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success);
|
||||
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_UnknownTool_RecoversAndCompletes()
|
||||
{
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildToolCallResponse("UnknownTool", new { path = "x.txt" }, "unknown tool call"),
|
||||
BuildTextResponse("알 수 없는 도구를 정정하고 완료했습니다."),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "테스트 작업" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("완료");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.Error && e.ToolName == "UnknownTool");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_PlanModeAlways_EmitsPlanningThenExecutesTool()
|
||||
{
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildTextOnlyResponse("1. math_eval 도구로 계산\n2. 결과를 검증하고 보고"),
|
||||
BuildToolCallResponse("math_eval", new { expression = "10/2" }, "계획 실행"),
|
||||
BuildTextResponse("완료: 결과는 5"),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
settings.Settings.Llm.PlanMode = "always";
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "10/2 계산해줘" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("5");
|
||||
server.RequestCount.Should().BeGreaterThanOrEqualTo(3);
|
||||
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_AskPermissionDenied_EmitsPermissionEvents_AndCompletes()
|
||||
{
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildToolCallResponse("file_write", new { path = "deny-test.txt", content = "x" }, "파일 작성 시도"),
|
||||
BuildTextResponse("권한 거부를 확인하고 완료했습니다."),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
settings.Settings.Llm.DefaultAgentPermission = "Ask";
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
|
||||
loop.AskPermissionCallback = (_, _) => Task.FromResult(false);
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "파일을 저장해줘" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("완료");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.PermissionRequest && e.ToolName == "file_write");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.PermissionDenied && e.ToolName == "file_write");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_PreHookInputMutation_ChangesToolArguments()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-hook-e2e-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var scriptPath = Path.Combine(tempDir, "mutate_math.cmd");
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
scriptPath,
|
||||
"@echo {\"updatedInput\":{\"expression\":\"2+3\"},\"message\":\"hook applied\"}",
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildToolCallResponse("math_eval", new { expression = "1+1" }, "hook test"),
|
||||
BuildTextResponse("최종 완료"),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
settings.Settings.Llm.EnableToolHooks = true;
|
||||
settings.Settings.Llm.EnableHookInputMutation = true;
|
||||
settings.Settings.Llm.AgentHooks =
|
||||
[
|
||||
new AgentHookEntry
|
||||
{
|
||||
Name = "mutate-math",
|
||||
ToolName = "math_eval",
|
||||
Timing = "pre",
|
||||
ScriptPath = scriptPath,
|
||||
Enabled = true
|
||||
}
|
||||
];
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "1+1 계산" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("완료");
|
||||
events.Should().Contain(e => e.Type == AgentEventType.HookResult && e.Success);
|
||||
events.Should().Contain(e =>
|
||||
e.Type == AgentEventType.ToolResult &&
|
||||
e.ToolName == "math_eval" &&
|
||||
e.Summary.Contains("Result: 5", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_DisallowedTool_ByRuntimePolicy_EmitsPolicyRecoveryError()
|
||||
{
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildToolCallResponse("file_read", new { path = "README.md" }, "disallowed tool call"),
|
||||
BuildTextResponse("정책 위반 복구 후 종료"),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = """
|
||||
[Skill Runtime Policy]
|
||||
- allowed_tools: math_eval
|
||||
"""
|
||||
},
|
||||
new ChatMessage { Role = "user", Content = "파일 읽기" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("종료");
|
||||
events.Should().Contain(e =>
|
||||
e.Type == AgentEventType.Error &&
|
||||
e.ToolName == "file_read" &&
|
||||
e.Summary.Contains("허용되지 않은 도구", StringComparison.OrdinalIgnoreCase));
|
||||
events.Should().Contain(e => e.Type == AgentEventType.Complete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_HookFilters_ExecuteOnlyMatchingHookForToolAndTiming()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-hook-filter-e2e-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var hookAScript = Path.Combine(tempDir, "hook_a.cmd");
|
||||
var hookBScript = Path.Combine(tempDir, "hook_b.cmd");
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
hookAScript,
|
||||
"@echo {\"message\":\"hook-a hit\"}",
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
await File.WriteAllTextAsync(
|
||||
hookBScript,
|
||||
"@echo {\"message\":\"hook-b hit\"}",
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
using var server = new FakeOllamaServer(
|
||||
[
|
||||
BuildToolCallResponse("math_eval", new { expression = "2+2" }, "hook filter test"),
|
||||
BuildTextResponse("완료"),
|
||||
]);
|
||||
|
||||
var settings = BuildLoopSettings(server.Endpoint);
|
||||
settings.Settings.Llm.EnableToolHooks = true;
|
||||
settings.Settings.Llm.AgentHooks =
|
||||
[
|
||||
new AgentHookEntry
|
||||
{
|
||||
Name = "hook-a",
|
||||
ToolName = "math_eval",
|
||||
Timing = "pre",
|
||||
ScriptPath = hookAScript,
|
||||
Enabled = true
|
||||
},
|
||||
new AgentHookEntry
|
||||
{
|
||||
Name = "hook-b",
|
||||
ToolName = "math_eval",
|
||||
Timing = "pre",
|
||||
ScriptPath = hookBScript,
|
||||
Enabled = true
|
||||
}
|
||||
];
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
using var tools = ToolRegistry.CreateDefault();
|
||||
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
|
||||
|
||||
var events = new List<AgentEvent>();
|
||||
loop.EventOccurred += evt => events.Add(evt);
|
||||
|
||||
var result = await loop.RunAsync(
|
||||
[
|
||||
new ChatMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = """
|
||||
[Skill Runtime Policy]
|
||||
- allowed_tools: math_eval
|
||||
- hook_filters: hook-a@pre@math_eval
|
||||
"""
|
||||
},
|
||||
new ChatMessage { Role = "user", Content = "2+2 계산" }
|
||||
]);
|
||||
|
||||
result.Should().Contain("완료");
|
||||
events.Should().Contain(e =>
|
||||
e.Type == AgentEventType.HookResult &&
|
||||
e.Summary.Contains("[Hook:hook-a]", StringComparison.OrdinalIgnoreCase));
|
||||
events.Should().NotContain(e =>
|
||||
e.Type == AgentEventType.HookResult &&
|
||||
e.Summary.Contains("[Hook:hook-b]", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static SettingsService BuildLoopSettings(string endpoint)
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.OperationMode = OperationModePolicy.ExternalMode;
|
||||
settings.Settings.Llm.Service = "ollama";
|
||||
settings.Settings.Llm.OllamaEndpoint = endpoint;
|
||||
settings.Settings.Llm.Endpoint = endpoint;
|
||||
settings.Settings.Llm.Model = "test-model";
|
||||
settings.Settings.Llm.MaxAgentIterations = 6;
|
||||
settings.Settings.Llm.MaxRetryOnError = 1;
|
||||
settings.Settings.Llm.PlanMode = "off";
|
||||
settings.Settings.Llm.EnableToolHooks = false;
|
||||
settings.Settings.Llm.EnableAutoRouter = false;
|
||||
settings.Settings.Llm.EnableForkSkillDelegationEnforcement = true;
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static string BuildToolCallResponse(string toolName, object args, string content)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
message = new
|
||||
{
|
||||
content,
|
||||
tool_calls = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
id = "call_1",
|
||||
type = "function",
|
||||
function = new
|
||||
{
|
||||
name = toolName,
|
||||
arguments = args
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return JsonSerializer.Serialize(payload);
|
||||
}
|
||||
|
||||
private static string BuildTextResponse(string content)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
message = new
|
||||
{
|
||||
content
|
||||
}
|
||||
};
|
||||
return JsonSerializer.Serialize(payload);
|
||||
}
|
||||
|
||||
private static string BuildTextOnlyResponse(string content)
|
||||
=> BuildTextResponse(content);
|
||||
|
||||
private sealed class FakeOllamaServer : IDisposable
|
||||
{
|
||||
private readonly HttpListener _listener;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _serveTask;
|
||||
private readonly Queue<string> _responses;
|
||||
private readonly string _fallbackResponse;
|
||||
private int _requestCount;
|
||||
|
||||
public string Endpoint { get; }
|
||||
public int RequestCount => _requestCount;
|
||||
|
||||
public FakeOllamaServer(IEnumerable<string> responses)
|
||||
{
|
||||
_responses = new Queue<string>(responses);
|
||||
_fallbackResponse = _responses.Count > 0 ? _responses.Last() : BuildTextResponse("(empty)");
|
||||
|
||||
var port = GetFreePort();
|
||||
Endpoint = $"http://127.0.0.1:{port}";
|
||||
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add($"{Endpoint}/");
|
||||
_listener.Start();
|
||||
|
||||
_serveTask = Task.Run(() => ServeLoopAsync(_cts.Token));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
try { _listener.Stop(); } catch { }
|
||||
try { _listener.Close(); } catch { }
|
||||
try { _serveTask.Wait(TimeSpan.FromSeconds(2)); } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
|
||||
private async Task ServeLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
HttpListenerContext? context = null;
|
||||
try
|
||||
{
|
||||
context = await _listener.GetContextAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Interlocked.Increment(ref _requestCount);
|
||||
using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding ?? Encoding.UTF8);
|
||||
_ = await reader.ReadToEndAsync();
|
||||
|
||||
var body = _responses.Count > 0 ? _responses.Dequeue() : _fallbackResponse;
|
||||
var bytes = Encoding.UTF8.GetBytes(body);
|
||||
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.ContentEncoding = Encoding.UTF8;
|
||||
context.Response.ContentLength64 = bytes.Length;
|
||||
await context.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length, ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 응답 실패는 테스트 종료 과정에서 발생 가능
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { context.Response.OutputStream.Close(); } catch { }
|
||||
try { context.Response.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/AxCopilot.Tests/Services/AgentStatsServiceTests.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Reflection;
|
||||
using AxCopilot.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class AgentStatsServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void CalculateRetryQualityRate_ReturnsOneWhenNoSignals()
|
||||
{
|
||||
var rate = InvokePrivateStatic<double>(
|
||||
"CalculateRetryQualityRate",
|
||||
0,
|
||||
0);
|
||||
|
||||
rate.Should().Be(1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateRetryQualityRate_ReturnsRecoveryRatio()
|
||||
{
|
||||
var rate = InvokePrivateStatic<double>(
|
||||
"CalculateRetryQualityRate",
|
||||
3,
|
||||
1);
|
||||
|
||||
rate.Should().BeApproximately(0.75, 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSummary_ComputesTaskTypeRetryQualityBreakdown()
|
||||
{
|
||||
var records = new List<AgentStatsService.AgentSessionRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TaskType = "bugfix",
|
||||
ToolCalls = 3,
|
||||
InputTokens = 10,
|
||||
OutputTokens = 5,
|
||||
RecoveredAfterFailureCount = 2,
|
||||
RepeatedFailureBlockedCount = 1
|
||||
},
|
||||
new()
|
||||
{
|
||||
TaskType = "feature",
|
||||
ToolCalls = 4,
|
||||
InputTokens = 12,
|
||||
OutputTokens = 7,
|
||||
RecoveredAfterFailureCount = 1,
|
||||
RepeatedFailureBlockedCount = 1
|
||||
}
|
||||
};
|
||||
|
||||
var summary = InvokePrivateStatic<AgentStatsService.AgentStatsSummary>(
|
||||
"BuildSummary",
|
||||
records);
|
||||
|
||||
summary.TaskTypeBreakdown["bugfix"].Should().Be(1);
|
||||
summary.TaskTypeBreakdown["feature"].Should().Be(1);
|
||||
summary.RetryQualityByTaskType["bugfix"].Should().BeApproximately(2.0 / 3.0, 0.0001);
|
||||
summary.RetryQualityByTaskType["feature"].Should().BeApproximately(0.5, 0.0001);
|
||||
}
|
||||
|
||||
private static T InvokePrivateStatic<T>(string methodName, params object?[] arguments)
|
||||
{
|
||||
var method = typeof(AgentStatsService).GetMethod(
|
||||
methodName,
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull($"{methodName} should exist on AgentStatsService");
|
||||
|
||||
var result = method!.Invoke(null, arguments);
|
||||
result.Should().NotBeNull();
|
||||
return (T)result!;
|
||||
}
|
||||
}
|
||||
877
src/AxCopilot.Tests/Services/AppStateServiceTests.cs
Normal file
@@ -0,0 +1,877 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class AppStateServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void LoadFromSettings_ReflectsPermissionAndMcpSummary()
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Llm.FilePermission = "Auto";
|
||||
settings.Settings.Llm.AgentDecisionLevel = "normal";
|
||||
settings.Settings.Llm.PlanMode = "always";
|
||||
settings.Settings.Llm.ToolPermissions["process"] = "Deny";
|
||||
settings.Settings.Llm.EnableSkillSystem = true;
|
||||
settings.Settings.Llm.SkillsFolderPath = @"C:\skills";
|
||||
settings.Settings.Llm.McpServers =
|
||||
[
|
||||
new McpServerEntry { Name = "one", Enabled = true, Transport = "stdio" },
|
||||
new McpServerEntry { Name = "two", Enabled = false, Transport = "stdio" },
|
||||
];
|
||||
|
||||
var state = new AppStateService();
|
||||
|
||||
state.LoadFromSettings(settings);
|
||||
|
||||
state.Permissions.FilePermission.Should().Be("Auto");
|
||||
state.Permissions.AgentDecisionLevel.Should().Be("normal");
|
||||
state.Permissions.PlanMode.Should().Be("always");
|
||||
state.Permissions.ToolOverrideCount.Should().Be(1);
|
||||
state.Permissions.ToolOverrides.Should().ContainSingle();
|
||||
state.Skills.Enabled.Should().BeTrue();
|
||||
state.Skills.FolderPath.Should().Be(@"C:\skills");
|
||||
state.Mcp.ConfiguredServerCount.Should().Be(2);
|
||||
state.Mcp.EnabledServerCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefreshSkillCatalog_ComputesCounts()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var skills = new[]
|
||||
{
|
||||
new SkillDefinition { Name = "alpha", IsAvailable = true },
|
||||
new SkillDefinition { Name = "beta", IsAvailable = false },
|
||||
new SkillDefinition { Name = "gamma", IsAvailable = true },
|
||||
};
|
||||
|
||||
state.RefreshSkillCatalog(skills, @"D:\skills", enabled: true);
|
||||
|
||||
state.Skills.LoadedCount.Should().Be(3);
|
||||
state.Skills.AvailableCount.Should().Be(2);
|
||||
state.Skills.UnavailableCount.Should().Be(1);
|
||||
state.Skills.FolderPath.Should().Be(@"D:\skills");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefreshAgentCatalog_ComputesToolTypeBreakdown()
|
||||
{
|
||||
using var registry = new ToolRegistry();
|
||||
registry.Register(new McpTool(new McpClientService(new McpServerEntry
|
||||
{
|
||||
Name = "fake",
|
||||
Enabled = true,
|
||||
Transport = "stdio",
|
||||
Command = "fake"
|
||||
}), new McpToolDefinition
|
||||
{
|
||||
Name = "mcp_lookup",
|
||||
Description = "lookup"
|
||||
}));
|
||||
registry.Register(new ExcelSkill());
|
||||
registry.Register(new FileReadTool());
|
||||
|
||||
var state = new AppStateService();
|
||||
state.RefreshAgentCatalog(registry);
|
||||
|
||||
state.AgentCatalog.RegisteredToolCount.Should().Be(3);
|
||||
state.AgentCatalog.McpToolCount.Should().Be(1);
|
||||
state.AgentCatalog.SkillToolCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertTask_UpdatesExistingTaskInsteadOfDuplicating()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.UpsertTask("tool:1", "tool", "file_read", "first", "running");
|
||||
state.UpsertTask("tool:1", "tool", "file_read", "updated", "running", @"E:\a.txt");
|
||||
|
||||
state.ActiveTasks.Should().HaveCount(1);
|
||||
state.ActiveTasks[0].Summary.Should().Be("updated");
|
||||
state.ActiveTasks[0].FilePath.Should().Be(@"E:\a.txt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteTask_MovesTaskFromActiveToRecent()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
state.UpsertTask("agent:run1", "agent", "main", "working", "running");
|
||||
|
||||
state.CompleteTask("agent:run1", "done", "completed");
|
||||
|
||||
state.ActiveTasks.Should().BeEmpty();
|
||||
state.RecentTasks.Should().ContainSingle();
|
||||
state.RecentTasks[0].Status.Should().Be("completed");
|
||||
state.RecentTasks[0].Summary.Should().Be("done");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearTasksByPrefix_MovesMatchingTasksOnly()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
state.UpsertTask("tool:run1:file_read", "tool", "file_read", "reading", "running");
|
||||
state.UpsertTask("tool:run1:grep", "tool", "grep", "searching", "running");
|
||||
state.UpsertTask("agent:run1", "agent", "main", "planning", "running");
|
||||
|
||||
state.ClearTasksByPrefix("tool:run1:", "tool batch done", "completed");
|
||||
|
||||
state.ActiveTasks.Should().ContainSingle(t => t.Id == "agent:run1");
|
||||
state.RecentTasks.Should().HaveCount(2);
|
||||
state.RecentTasks.Should().OnlyContain(t => t.Status == "completed");
|
||||
state.RecentTasks.Should().OnlyContain(t => t.Summary == "tool batch done");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteTask_KeepsOnlyRecentTwentyFiveItems()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
var id = $"tool:{i}";
|
||||
state.UpsertTask(id, "tool", $"tool-{i}", $"summary-{i}", "running");
|
||||
state.CompleteTask(id, $"done-{i}", "completed");
|
||||
}
|
||||
|
||||
state.RecentTasks.Should().HaveCount(25);
|
||||
state.RecentTasks[0].Id.Should().Be("tool:29");
|
||||
state.RecentTasks[^1].Id.Should().Be("tool:5");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StateChanged_FiresWhenTaskStateChanges()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var count = 0;
|
||||
state.StateChanged += () => count++;
|
||||
|
||||
state.UpsertTask("agent:run1", "agent", "main", "thinking", "running");
|
||||
state.CompleteTask("agent:run1", "done", "completed");
|
||||
|
||||
count.Should().BeGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChatExecutionEvent_Serialization_PreservesRunId()
|
||||
{
|
||||
var original = new ChatExecutionEvent
|
||||
{
|
||||
RunId = "run-123",
|
||||
Type = "ToolCall",
|
||||
ToolName = "file_read",
|
||||
Summary = "reading"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original);
|
||||
var restored = JsonSerializer.Deserialize<ChatExecutionEvent>(json);
|
||||
|
||||
restored.Should().NotBeNull();
|
||||
restored!.RunId.Should().Be("run-123");
|
||||
restored.ToolName.Should().Be("file_read");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDraftQueueItems_ReadsAttachedChatSessionItems()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var session = new ChatSessionStateService();
|
||||
state.AttachChatSession(session);
|
||||
session.EnqueueDraft("Chat", "draft one", "next");
|
||||
|
||||
var items = state.GetDraftQueueItems("Chat");
|
||||
|
||||
items.Should().ContainSingle();
|
||||
items[0].Text.Should().Be("draft one");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDraftQueueSummary_MapsBlockedDraftCounts()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var session = new ChatSessionStateService();
|
||||
state.AttachChatSession(session);
|
||||
var item = session.EnqueueDraft("Chat", "draft one", "next");
|
||||
session.MarkDraftRunning("Chat", item!.Id);
|
||||
session.ScheduleDraftRetry("Chat", item.Id, "temporary", maxAutoRetries: 3);
|
||||
|
||||
var summary = state.GetDraftQueueSummary("Chat");
|
||||
|
||||
summary.BlockedCount.Should().Be(1);
|
||||
summary.NextReadyAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAgentEvent_StoresRecentPermissionHistory()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.PermissionRequest,
|
||||
ToolName = "file_write",
|
||||
Summary = "권한 확인 필요 · 대상: a.txt",
|
||||
Timestamp = DateTime.Now.AddSeconds(-2),
|
||||
});
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.PermissionDenied,
|
||||
ToolName = "file_write",
|
||||
Summary = "권한 거부",
|
||||
Timestamp = DateTime.Now,
|
||||
});
|
||||
|
||||
state.GetRecentPermissionEvents().Should().HaveCount(2);
|
||||
state.GetRecentPermissionEvents()[0].Status.Should().Be("denied");
|
||||
state.GetLatestDeniedPermission().Should().NotBeNull();
|
||||
state.FormatPermissionEventLine(state.GetRecentPermissionEvents()[0]).Should().Contain("권한 거부");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAgentEvent_StoresRecentHookHistory()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-hook",
|
||||
Type = AgentEventType.HookResult,
|
||||
ToolName = "file_write",
|
||||
Summary = "[Hook:precheck] ok",
|
||||
Timestamp = DateTime.Now,
|
||||
Success = true,
|
||||
});
|
||||
|
||||
state.GetRecentHookEvents().Should().ContainSingle();
|
||||
state.GetRecentHookEvents()[0].ToolName.Should().Be("file_write");
|
||||
state.FormatHookEventLine(state.GetRecentHookEvents()[0]).Should().Contain("hook");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySubAgentStatus_StoresBackgroundJobHistory()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-1",
|
||||
Task = "scan",
|
||||
Status = SubAgentRunStatus.Started,
|
||||
Summary = "started",
|
||||
});
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-1",
|
||||
Task = "scan",
|
||||
Status = SubAgentRunStatus.Completed,
|
||||
Summary = "completed",
|
||||
});
|
||||
|
||||
state.GetBackgroundJobSummary().ActiveCount.Should().Be(0);
|
||||
state.GetRecentBackgroundJobs().Should().ContainSingle();
|
||||
state.GetRecentBackgroundJobs()[0].Title.Should().Be("worker-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBackgroundJobSummary_ReturnsActiveJobCount()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-2",
|
||||
Task = "index",
|
||||
Status = SubAgentRunStatus.Started,
|
||||
Summary = "running",
|
||||
});
|
||||
|
||||
var summary = state.GetBackgroundJobSummary();
|
||||
|
||||
summary.ActiveCount.Should().Be(1);
|
||||
summary.LatestRecent.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetActiveBackgroundJobs_ReturnsActiveItems()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-2",
|
||||
Task = "index",
|
||||
Status = SubAgentRunStatus.Started,
|
||||
Summary = "running",
|
||||
});
|
||||
|
||||
state.GetActiveBackgroundJobs().Should().ContainSingle();
|
||||
state.GetActiveBackgroundJobs()[0].Id.Should().Contain("worker-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOperationalStatus_PrioritizesQueueAndBackgroundSignals()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var session = new ChatSessionStateService();
|
||||
state.AttachChatSession(session);
|
||||
|
||||
var draft = session.EnqueueDraft("Chat", "follow up", "next");
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-3",
|
||||
Task = "scan",
|
||||
Status = SubAgentRunStatus.Started,
|
||||
Summary = "running",
|
||||
});
|
||||
|
||||
var status = state.GetOperationalStatus("Chat");
|
||||
|
||||
status.ShowRuntimeBadge.Should().BeTrue();
|
||||
status.RuntimeLabel.Should().Be("실행 중 2");
|
||||
status.StripKind.Should().Be("running");
|
||||
status.StripText.Should().Contain("진행 중 2");
|
||||
draft.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPermissionSummary_UsesConversationOverrideWhenPresent()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Llm.FilePermission = "Ask";
|
||||
settings.Settings.Llm.ToolPermissions["process"] = "Deny";
|
||||
state.LoadFromSettings(settings);
|
||||
|
||||
var summary = state.GetPermissionSummary(new ChatConversation { Permission = "Auto" });
|
||||
|
||||
summary.EffectiveMode.Should().Be("Auto");
|
||||
summary.DefaultMode.Should().Be("Ask");
|
||||
summary.OverrideCount.Should().Be(1);
|
||||
summary.RiskLevel.Should().Be("high");
|
||||
summary.TopOverrides.Should().ContainSingle();
|
||||
summary.TopOverrides[0].Key.Should().Be("process");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertAgentRun_TracksLatestRunState()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.UpsertAgentRun("run-42", "running", "planning", 3);
|
||||
|
||||
state.AgentRun.RunId.Should().Be("run-42");
|
||||
state.AgentRun.Status.Should().Be("running");
|
||||
state.AgentRun.Summary.Should().Be("planning");
|
||||
state.AgentRun.LastIteration.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteAgentRun_StoresFinalState()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
state.UpsertAgentRun("run-42", "running", "planning", 2);
|
||||
|
||||
state.CompleteAgentRun("run-42", "completed", "done", 5);
|
||||
|
||||
state.AgentRun.RunId.Should().Be("run-42");
|
||||
state.AgentRun.Status.Should().Be("completed");
|
||||
state.AgentRun.Summary.Should().Be("done");
|
||||
state.AgentRun.LastIteration.Should().Be(5);
|
||||
state.AgentRunHistory.Should().ContainSingle();
|
||||
state.AgentRunHistory[0].RunId.Should().Be("run-42");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteAgentRun_KeepsOnlyRecentTwelveHistoryItems()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
state.UpsertAgentRun($"run-{i}", "running", $"summary-{i}", i);
|
||||
state.CompleteAgentRun($"run-{i}", "completed", $"done-{i}", i);
|
||||
}
|
||||
|
||||
state.AgentRunHistory.Should().HaveCount(12);
|
||||
state.AgentRunHistory[0].RunId.Should().Be("run-14");
|
||||
state.AgentRunHistory[^1].RunId.Should().Be("run-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreAgentRunHistory_LoadsLatestItemsInDescendingOrder()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
state.RestoreAgentRunHistory(
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-1", Status = "completed", Summary = "first", LastIteration = 1, UpdatedAt = now.AddMinutes(-2) },
|
||||
new ChatAgentRunRecord { RunId = "run-2", Status = "failed", Summary = "second", LastIteration = 2, UpdatedAt = now.AddMinutes(-1) },
|
||||
]);
|
||||
|
||||
state.AgentRunHistory.Should().HaveCount(2);
|
||||
state.AgentRunHistory[0].RunId.Should().Be("run-2");
|
||||
state.AgentRunHistory[1].RunId.Should().Be("run-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void RestoreCurrentAgentRun_PrefersRunningExecutionEventOverHistory()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
state.RestoreCurrentAgentRun(
|
||||
[
|
||||
new ChatExecutionEvent { RunId = "run-live", Type = "Thinking", Summary = "진행 중", Iteration = 5, Timestamp = now }
|
||||
],
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-old", Status = "completed", Summary = "완료", LastIteration = 2, UpdatedAt = now.AddMinutes(-1) }
|
||||
]);
|
||||
|
||||
state.AgentRun.RunId.Should().Be("run-live");
|
||||
state.AgentRun.Status.Should().Be("running");
|
||||
state.AgentRun.Summary.Should().Be("진행 중");
|
||||
state.AgentRun.LastIteration.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void RestoreCurrentAgentRun_UsesLatestRunHistoryWhenEventIsTerminal()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
state.RestoreCurrentAgentRun(
|
||||
[
|
||||
new ChatExecutionEvent { RunId = "run-x", Type = "Complete", Summary = "끝", Iteration = 6, Timestamp = now }
|
||||
],
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-x", Status = "completed", Summary = "끝", LastIteration = 6, StartedAt = now.AddMinutes(-2), UpdatedAt = now }
|
||||
]);
|
||||
|
||||
state.AgentRun.RunId.Should().Be("run-x");
|
||||
state.AgentRun.Status.Should().Be("completed");
|
||||
state.AgentRun.Summary.Should().Be("끝");
|
||||
state.AgentRun.LastIteration.Should().Be(6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void RestoreRecentTasks_RebuildsRecentTaskTimelineFromExecutionEvents()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
state.RestoreRecentTasks(
|
||||
[
|
||||
new ChatExecutionEvent { Timestamp = now.AddMinutes(-3), RunId = "run-1", Type = "ToolResult", ToolName = "file_read", Summary = "읽기 완료", Success = true },
|
||||
new ChatExecutionEvent { Timestamp = now.AddMinutes(-2), RunId = "run-1", Type = "PermissionDenied", ToolName = "file_write", Summary = "권한 거부", Success = false },
|
||||
new ChatExecutionEvent { Timestamp = now.AddMinutes(-1), RunId = "run-1", Type = "Complete", Summary = "작업 완료", Success = true },
|
||||
]);
|
||||
|
||||
state.ActiveTasks.Should().BeEmpty();
|
||||
state.RecentTasks.Should().HaveCount(3);
|
||||
state.RecentTasks[0].Kind.Should().Be("agent");
|
||||
state.RecentTasks[1].Kind.Should().Be("permission");
|
||||
state.RecentTasks[1].Status.Should().Be("failed");
|
||||
state.RecentTasks[2].Kind.Should().Be("tool");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAgentEvent_TracksPermissionLifecycleInTaskStore()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var request = new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.PermissionRequest,
|
||||
ToolName = "file_write",
|
||||
Summary = "권한 확인 필요",
|
||||
};
|
||||
|
||||
state.ApplyAgentEvent(request);
|
||||
state.ActiveTasks.Should().ContainSingle();
|
||||
state.ActiveTasks[0].Kind.Should().Be("permission");
|
||||
state.ActiveTasks[0].Status.Should().Be("waiting");
|
||||
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.PermissionGranted,
|
||||
ToolName = "file_write",
|
||||
Summary = "승인됨",
|
||||
});
|
||||
|
||||
state.ActiveTasks.Should().BeEmpty();
|
||||
state.RecentTasks.Should().ContainSingle();
|
||||
state.RecentTasks[0].Kind.Should().Be("permission");
|
||||
state.RecentTasks[0].Status.Should().Be("completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAgentEvent_TracksAgentRunCompletion()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-42",
|
||||
Type = AgentEventType.Thinking,
|
||||
Summary = "작업 준비",
|
||||
Iteration = 1,
|
||||
});
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-42",
|
||||
Type = AgentEventType.Complete,
|
||||
Summary = "작업 완료",
|
||||
Iteration = 3,
|
||||
});
|
||||
|
||||
state.AgentRun.RunId.Should().Be("run-42");
|
||||
state.AgentRun.Status.Should().Be("completed");
|
||||
state.RecentTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySubAgentStatus_TracksSubAgentLifecycle()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "research-1",
|
||||
Task = "scan project",
|
||||
Status = SubAgentRunStatus.Started,
|
||||
Summary = "started",
|
||||
});
|
||||
|
||||
state.ActiveTasks.Should().HaveCount(2);
|
||||
state.ActiveTasks.Should().Contain(x => x.Kind == "subagent");
|
||||
state.ActiveTasks.Should().Contain(x => x.Kind == "background");
|
||||
|
||||
state.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "research-1",
|
||||
Task = "scan project",
|
||||
Status = SubAgentRunStatus.Completed,
|
||||
Summary = "done",
|
||||
});
|
||||
|
||||
state.ActiveTasks.Should().BeEmpty();
|
||||
state.RecentTasks.Should().Contain(t => t.Kind == "subagent" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTaskSummary_ReturnsPendingPermissionAndLatestFailure()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-0",
|
||||
Type = AgentEventType.ToolCall,
|
||||
ToolName = "file_read",
|
||||
Summary = "읽는 중",
|
||||
});
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-0",
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "file_read",
|
||||
Summary = "완료",
|
||||
Success = true,
|
||||
});
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.PermissionRequest,
|
||||
ToolName = "file_write",
|
||||
Summary = "권한 확인 필요",
|
||||
});
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-2",
|
||||
Type = AgentEventType.Thinking,
|
||||
Summary = "실패 전 준비",
|
||||
Iteration = 1,
|
||||
});
|
||||
state.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-2",
|
||||
Type = AgentEventType.Error,
|
||||
Summary = "실패",
|
||||
Iteration = 2,
|
||||
});
|
||||
|
||||
var summary = state.GetTaskSummary();
|
||||
|
||||
summary.ActiveCount.Should().Be(1);
|
||||
summary.PendingPermissionCount.Should().Be(1);
|
||||
summary.LatestRecentTask.Should().NotBeNull();
|
||||
summary.LatestFailedRun.Should().NotBeNull();
|
||||
summary.LatestFailedRun!.RunId.Should().Be("run-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRecentAgentRuns_AndLatestFailedRun_ReturnExpectedItems()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
|
||||
state.UpsertAgentRun("run-1", "running", "one", 1);
|
||||
state.CompleteAgentRun("run-1", "completed", "done-1", 1);
|
||||
state.UpsertAgentRun("run-2", "running", "two", 2);
|
||||
state.CompleteAgentRun("run-2", "failed", "done-2", 2);
|
||||
state.UpsertAgentRun("run-3", "running", "three", 3);
|
||||
state.CompleteAgentRun("run-3", "completed", "done-3", 3);
|
||||
|
||||
var recent = state.GetRecentAgentRuns(2);
|
||||
var latestFailed = state.GetLatestFailedRun();
|
||||
|
||||
recent.Should().HaveCount(2);
|
||||
recent[0].RunId.Should().Be("run-3");
|
||||
recent[1].RunId.Should().Be("run-2");
|
||||
latestFailed.Should().NotBeNull();
|
||||
latestFailed!.RunId.Should().Be("run-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDraftQueueSummary_ReadsAttachedChatSessionQueue()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var session = new ChatSessionStateService();
|
||||
state.AttachChatSession(session);
|
||||
|
||||
var first = session.EnqueueDraft("Chat", "first", "next");
|
||||
var second = session.EnqueueDraft("Chat", "second", "now");
|
||||
session.MarkDraftRunning("Chat", second!.Id);
|
||||
session.MarkDraftCompleted("Chat", second.Id);
|
||||
|
||||
var summary = state.GetDraftQueueSummary("Chat");
|
||||
|
||||
summary.TotalCount.Should().Be(2);
|
||||
summary.QueuedCount.Should().Be(1);
|
||||
summary.CompletedCount.Should().Be(1);
|
||||
summary.NextItem.Should().NotBeNull();
|
||||
summary.NextItem!.Id.Should().Be(first!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConversationRunSummary_ComputesConversationSidebarMeta()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
var summary = state.GetConversationRunSummary(
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-3", Status = "completed", Summary = "latest", UpdatedAt = now },
|
||||
new ChatAgentRunRecord { RunId = "run-2", Status = "failed", Summary = "failed", UpdatedAt = now.AddMinutes(-1) },
|
||||
new ChatAgentRunRecord { RunId = "run-1", Status = "completed", Summary = "older", UpdatedAt = now.AddMinutes(-2) },
|
||||
]);
|
||||
|
||||
summary.AgentRunCount.Should().Be(3);
|
||||
summary.FailedAgentRunCount.Should().Be(1);
|
||||
summary.LastAgentRunSummary.Should().Be("latest");
|
||||
summary.LastFailedAt.Should().Be(now.AddMinutes(-1));
|
||||
summary.LastCompletedAt.Should().Be(now);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAgentRunById_AndLatestConversationRun_ReturnExpectedRun()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
state.UpsertAgentRun("run-1", "running", "one", 1);
|
||||
state.CompleteAgentRun("run-1", "completed", "one-done", 1);
|
||||
state.UpsertAgentRun("run-2", "running", "two", 2);
|
||||
state.CompleteAgentRun("run-2", "failed", "two-failed", 2);
|
||||
|
||||
var byId = state.GetAgentRunById("run-2");
|
||||
var now = DateTime.Now;
|
||||
var latestConversation = state.GetLatestConversationRun(
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-2", UpdatedAt = now.AddMinutes(1) },
|
||||
new ChatAgentRunRecord { RunId = "run-1", UpdatedAt = now },
|
||||
]);
|
||||
|
||||
byId.Should().NotBeNull();
|
||||
byId!.RunId.Should().Be("run-2");
|
||||
latestConversation.Should().NotBeNull();
|
||||
latestConversation!.RunId.Should().Be("run-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLatestConversationRun_UsesTimestampWhenHistoryOrderIsUnsorted()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
state.UpsertAgentRun("run-1", "running", "one", 1);
|
||||
state.CompleteAgentRun("run-1", "completed", "one-done", 1);
|
||||
state.UpsertAgentRun("run-2", "running", "two", 2);
|
||||
state.CompleteAgentRun("run-2", "failed", "two-failed", 2);
|
||||
|
||||
var now = DateTime.Now;
|
||||
var latestConversation = state.GetLatestConversationRun(
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-1", UpdatedAt = now },
|
||||
new ChatAgentRunRecord { RunId = "run-2", UpdatedAt = now.AddMinutes(1) },
|
||||
]);
|
||||
|
||||
latestConversation.Should().NotBeNull();
|
||||
latestConversation!.RunId.Should().Be("run-2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetConversationRunSummary_UsesLatestByTimestampWhenUnsorted()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
var summary = state.GetConversationRunSummary(
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-1", Status = "completed", Summary = "older shown first", UpdatedAt = now.AddMinutes(-5) },
|
||||
new ChatAgentRunRecord { RunId = "run-2", Status = "failed", Summary = "latest failed", UpdatedAt = now.AddMinutes(2) },
|
||||
new ChatAgentRunRecord { RunId = "run-3", Status = "completed", Summary = "latest completed", UpdatedAt = now.AddMinutes(1) },
|
||||
]);
|
||||
|
||||
summary.AgentRunCount.Should().Be(3);
|
||||
summary.FailedAgentRunCount.Should().Be(1);
|
||||
summary.LastAgentRunSummary.Should().Be("latest failed");
|
||||
summary.LastFailedAt.Should().Be(now.AddMinutes(2));
|
||||
summary.LastCompletedAt.Should().Be(now.AddMinutes(1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRunDetailSummary_ReturnsRecentEventsAndDistinctFiles()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
var summary = state.GetRunDetailSummary(
|
||||
[
|
||||
new ChatExecutionEvent { RunId = "run-1", Type = "ToolCall", Timestamp = now.AddMinutes(-3), FilePath = @"E:\a.cs" },
|
||||
new ChatExecutionEvent { RunId = "run-1", Type = "ToolResult", Timestamp = now.AddMinutes(-2), FilePath = @"E:\a.cs" },
|
||||
new ChatExecutionEvent { RunId = "run-1", Type = "Complete", Timestamp = now.AddMinutes(-1), FilePath = @"E:\b.cs" },
|
||||
new ChatExecutionEvent { RunId = "run-2", Type = "ToolCall", Timestamp = now, FilePath = @"E:\c.cs" },
|
||||
], "run-1", eventTake: 2, fileTake: 3);
|
||||
|
||||
summary.RunId.Should().Be("run-1");
|
||||
summary.Events.Should().HaveCount(2);
|
||||
summary.Events[0].Type.Should().Be("Complete");
|
||||
summary.Events[1].Type.Should().Be("ToolResult");
|
||||
summary.FilePaths.Should().Equal(@"E:\b.cs", @"E:\a.cs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRunDetailSummary_FilePathsFollowLatestEventOrderEvenWhenHistoryIsUnsorted()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
var summary = state.GetRunDetailSummary(
|
||||
[
|
||||
new ChatExecutionEvent { RunId = "run-x", Type = "ToolResult", Timestamp = now.AddMinutes(-10), FilePath = @"E:\old.cs" },
|
||||
new ChatExecutionEvent { RunId = "run-x", Type = "ToolResult", Timestamp = now.AddMinutes(-1), FilePath = @"E:\latest.cs" },
|
||||
new ChatExecutionEvent { RunId = "run-x", Type = "ToolResult", Timestamp = now.AddMinutes(-5), FilePath = @"E:\mid.cs" },
|
||||
new ChatExecutionEvent { RunId = "run-x", Type = "ToolResult", Timestamp = now.AddMinutes(-2), FilePath = @"E:\latest.cs" },
|
||||
], "run-x", eventTake: 4, fileTake: 3);
|
||||
|
||||
summary.FilePaths.Should().Equal(@"E:\latest.cs", @"E:\mid.cs", @"E:\old.cs");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRunDisplay_AndFormatExecutionEventLine_ReturnFormattedText()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var run = new AppStateService.AgentRunState
|
||||
{
|
||||
RunId = "run-123456789",
|
||||
Status = "completed",
|
||||
Summary = "요약",
|
||||
LastIteration = 4,
|
||||
UpdatedAt = new DateTime(2026, 4, 2, 9, 30, 0),
|
||||
};
|
||||
var evt = new ChatExecutionEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = "ToolResult",
|
||||
ToolName = "file_read",
|
||||
Summary = "읽기 완료",
|
||||
Timestamp = new DateTime(2026, 4, 2, 9, 31, 0),
|
||||
};
|
||||
|
||||
var display = state.GetRunDisplay(run);
|
||||
var line = state.FormatExecutionEventLine(evt);
|
||||
|
||||
display.HeaderText.Should().Be("run run-1234 · 완료");
|
||||
display.MetaText.Should().Be("iteration 4 · 09:30:00");
|
||||
display.SummaryText.Should().Be("요약");
|
||||
line.Should().Be("09:31:00 · file_read 결과 · 읽기 완료");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatExecutionEventLine_UsesDecisionLabel()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var evt = new ChatExecutionEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = "Decision",
|
||||
Summary = "계획 승인",
|
||||
Timestamp = new DateTime(2026, 4, 2, 9, 32, 0),
|
||||
};
|
||||
|
||||
var line = state.FormatExecutionEventLine(evt);
|
||||
|
||||
line.Should().Be("09:32:00 · 계획 승인 · 계획 승인");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRunPlanHistory_ReturnsOriginalRevisedAndFinalApprovedSnapshots()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var now = new DateTime(2026, 4, 3, 10, 0, 0);
|
||||
|
||||
var history = new List<ChatExecutionEvent>
|
||||
{
|
||||
new() { RunId = "run-1", Type = "Planning", Summary = "작업 계획: 2단계", Steps = new List<string> { "요구사항 분석", "초안 작성" }, Timestamp = now.AddSeconds(1) },
|
||||
new() { RunId = "run-1", Type = "Decision", Summary = "계획 수정 요청", Timestamp = now.AddSeconds(2) },
|
||||
new() { RunId = "run-1", Type = "Planning", Summary = "수정된 계획: 3단계", Steps = new List<string> { "요구사항 분석", "초안 작성", "검증" }, Timestamp = now.AddSeconds(3) },
|
||||
new() { RunId = "run-1", Type = "Decision", Summary = "계획 승인(편집 반영) · 3단계", Steps = new List<string> { "요구사항 분석", "초안 작성", "검증" }, Timestamp = now.AddSeconds(4) },
|
||||
new() { RunId = "run-2", Type = "Planning", Summary = "다른 실행", Steps = new List<string> { "무시" }, Timestamp = now.AddSeconds(5) },
|
||||
};
|
||||
|
||||
var planHistory = state.GetRunPlanHistory(history, "run-1");
|
||||
|
||||
planHistory.OriginalSummary.Should().Be("작업 계획: 2단계");
|
||||
planHistory.OriginalSteps.Should().Equal("요구사항 분석", "초안 작성");
|
||||
planHistory.RevisedSummary.Should().Be("수정된 계획: 3단계");
|
||||
planHistory.RevisedSteps.Should().Equal("요구사항 분석", "초안 작성", "검증");
|
||||
planHistory.FinalApprovedSummary.Should().Be("계획 승인(편집 반영) · 3단계");
|
||||
planHistory.FinalApprovedSteps.Should().Equal("요구사항 분석", "초안 작성", "검증");
|
||||
planHistory.HasAny.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatExecutionEventLine_UsesDecisionRejectedLabel()
|
||||
{
|
||||
var state = new AppStateService();
|
||||
var evt = new ChatExecutionEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = "Decision",
|
||||
Summary = "계획 반려 · 수정 요청",
|
||||
Timestamp = new DateTime(2026, 4, 2, 9, 33, 0),
|
||||
};
|
||||
|
||||
var line = state.FormatExecutionEventLine(evt);
|
||||
|
||||
line.Should().Be("09:33:00 · 계획 반려 · 계획 반려 · 수정 요청");
|
||||
}
|
||||
}
|
||||
21
src/AxCopilot.Tests/Services/BackgroundJobServiceTests.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class BackgroundJobServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void UpsertAndComplete_MoveJobFromActiveToRecent()
|
||||
{
|
||||
var service = new AxCopilot.Services.BackgroundJobService();
|
||||
|
||||
service.Upsert("job-1", "subagent", "worker-1", "scan");
|
||||
service.Complete("job-1", "done", "completed");
|
||||
|
||||
service.ActiveJobs.Should().BeEmpty();
|
||||
service.RecentJobs.Should().ContainSingle();
|
||||
service.RecentJobs[0].Status.Should().Be("completed");
|
||||
service.RecentJobs[0].Summary.Should().Be("done");
|
||||
}
|
||||
}
|
||||
704
src/AxCopilot.Tests/Services/ChatSessionStateServiceTests.cs
Normal file
@@ -0,0 +1,704 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class ChatSessionStateServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void AppendExecutionEvent_CreatesConversationAndTrimsToLatest400()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
for (var i = 0; i < 405; i++)
|
||||
{
|
||||
session.AppendExecutionEvent("Code", new AgentEvent
|
||||
{
|
||||
RunId = $"run-{i}",
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "file_read",
|
||||
Summary = $"event-{i}",
|
||||
Timestamp = DateTime.Now.AddSeconds(i),
|
||||
Success = true,
|
||||
});
|
||||
}
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Tab.Should().Be("Code");
|
||||
session.CurrentConversation.ExecutionEvents.Should().HaveCount(400);
|
||||
session.CurrentConversation.ExecutionEvents[0].RunId.Should().Be("run-5");
|
||||
session.CurrentConversation.ExecutionEvents[^1].RunId.Should().Be("run-404");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendExecutionEvent_MergesNearDuplicateEvents()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
session.AppendExecutionEvent("Code", new AgentEvent
|
||||
{
|
||||
RunId = "run-dup",
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "file_read",
|
||||
Summary = "same summary",
|
||||
Timestamp = now,
|
||||
Success = true,
|
||||
StepCurrent = 1,
|
||||
StepTotal = 3,
|
||||
InputTokens = 12,
|
||||
OutputTokens = 8,
|
||||
ElapsedMs = 20,
|
||||
});
|
||||
session.AppendExecutionEvent("Code", new AgentEvent
|
||||
{
|
||||
RunId = "run-dup",
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "file_read",
|
||||
Summary = "same summary",
|
||||
Timestamp = now.AddSeconds(1),
|
||||
Success = true,
|
||||
StepCurrent = 2,
|
||||
StepTotal = 3,
|
||||
InputTokens = 18,
|
||||
OutputTokens = 11,
|
||||
ElapsedMs = 40,
|
||||
});
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.ExecutionEvents.Should().HaveCount(1);
|
||||
session.CurrentConversation.ExecutionEvents[0].StepCurrent.Should().Be(2);
|
||||
session.CurrentConversation.ExecutionEvents[0].ElapsedMs.Should().Be(40);
|
||||
session.CurrentConversation.ExecutionEvents[0].InputTokens.Should().Be(18);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendAgentRun_KeepsLatestTwelveRuns()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
for (var i = 0; i < 15; i++)
|
||||
{
|
||||
session.AppendAgentRun("Cowork", new AgentEvent
|
||||
{
|
||||
RunId = $"run-{i}",
|
||||
Type = AgentEventType.Complete,
|
||||
Summary = $"summary-{i}",
|
||||
Timestamp = DateTime.Now.AddMinutes(-i),
|
||||
Iteration = i,
|
||||
}, "completed", $"done-{i}");
|
||||
}
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.AgentRunHistory.Should().HaveCount(12);
|
||||
session.CurrentConversation.AgentRunHistory[0].RunId.Should().Be("run-14");
|
||||
session.CurrentConversation.AgentRunHistory[^1].RunId.Should().Be("run-3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendAgentRun_UpsertsByRunIdKeepingLatestState()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var baseTime = DateTime.Now;
|
||||
|
||||
session.AppendAgentRun("Code", new AgentEvent
|
||||
{
|
||||
RunId = "run-dup",
|
||||
Type = AgentEventType.Thinking,
|
||||
Summary = "running",
|
||||
Timestamp = baseTime,
|
||||
Iteration = 1,
|
||||
}, "running", "running");
|
||||
|
||||
session.AppendAgentRun("Code", new AgentEvent
|
||||
{
|
||||
RunId = "run-dup",
|
||||
Type = AgentEventType.Complete,
|
||||
Summary = "completed",
|
||||
Timestamp = baseTime.AddSeconds(5),
|
||||
Iteration = 2,
|
||||
}, "completed", "completed");
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.AgentRunHistory.Should().HaveCount(1);
|
||||
session.CurrentConversation.AgentRunHistory[0].RunId.Should().Be("run-dup");
|
||||
session.CurrentConversation.AgentRunHistory[0].Status.Should().Be("completed");
|
||||
session.CurrentConversation.AgentRunHistory[0].LastIteration.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnqueueDraft_AndToggleExecutionHistory_UpdateConversationState()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
var item = session.EnqueueDraft("Chat", " follow up draft ", "now");
|
||||
var visible = session.ToggleExecutionHistory("Chat");
|
||||
|
||||
item.Should().NotBeNull();
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.DraftQueueItems.Should().ContainSingle();
|
||||
session.CurrentConversation.DraftQueueItems[0].Text.Should().Be("follow up draft");
|
||||
session.CurrentConversation.DraftQueueItems[0].Priority.Should().Be("now");
|
||||
visible.Should().BeFalse();
|
||||
session.CurrentConversation.ShowExecutionHistory.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDraftQueueItems_ReturnsSnapshotOfCurrentConversationQueue()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
session.EnqueueDraft("Chat", "first draft", "next");
|
||||
session.EnqueueDraft("Chat", "second draft", "later");
|
||||
|
||||
var items = session.GetDraftQueueItems("Chat");
|
||||
|
||||
items.Should().HaveCount(2);
|
||||
items[0].Text.Should().Be("first draft");
|
||||
items[1].Priority.Should().Be("later");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScheduleDraftRetry_RequeuesBeforeMaxAttempts()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var item = session.EnqueueDraft("Chat", "retry me", "next");
|
||||
session.MarkDraftRunning("Chat", item!.Id);
|
||||
|
||||
var scheduled = session.ScheduleDraftRetry("Chat", item.Id, "temporary", maxAutoRetries: 3);
|
||||
|
||||
scheduled.Should().BeTrue();
|
||||
session.CurrentConversation!.DraftQueueItems[0].State.Should().Be("queued");
|
||||
session.CurrentConversation!.DraftQueueItems[0].NextRetryAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveConversationListPreferences_PersistsFilterFlags()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
session.SaveConversationListPreferences("Chat", failedOnly: true, runningOnly: true, sortByRecent: true);
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.ConversationFailedOnlyFilter.Should().BeTrue();
|
||||
session.CurrentConversation.ConversationRunningOnlyFilter.Should().BeTrue();
|
||||
session.CurrentConversation.ConversationSortMode.Should().Be("recent");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearCurrentConversation_RemovesRememberedConversationIdForTab()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
session.RememberConversation("Code", "conv-1");
|
||||
session.ClearCurrentConversation("Code");
|
||||
|
||||
session.GetConversationId("Code").Should().BeNull();
|
||||
session.CurrentConversation.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveCurrentConversation_RemembersConversationId_WhenOnlyExecutionHistoryExists()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var conv = session.EnsureCurrentConversation("Code");
|
||||
conv.Messages.Clear();
|
||||
conv.ExecutionEvents = new List<ChatExecutionEvent>
|
||||
{
|
||||
new()
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = "ToolResult",
|
||||
ToolName = "file_read",
|
||||
Summary = "read",
|
||||
Timestamp = DateTime.Now
|
||||
}
|
||||
};
|
||||
conv.AgentRunHistory.Clear();
|
||||
conv.DraftQueueItems.Clear();
|
||||
|
||||
session.SaveCurrentConversation(storage, "Code");
|
||||
|
||||
session.GetConversationId("Code").Should().Be(conv.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveCurrentConversation_RemembersConversationId_WhenOnlyAgentRunHistoryExists()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var conv = session.EnsureCurrentConversation("Code");
|
||||
conv.Messages.Clear();
|
||||
conv.ExecutionEvents.Clear();
|
||||
conv.AgentRunHistory = new List<ChatAgentRunRecord>
|
||||
{
|
||||
new()
|
||||
{
|
||||
RunId = "run-2",
|
||||
Status = "completed",
|
||||
Summary = "done",
|
||||
UpdatedAt = DateTime.Now
|
||||
}
|
||||
};
|
||||
conv.DraftQueueItems.Clear();
|
||||
|
||||
session.SaveCurrentConversation(storage, "Code");
|
||||
|
||||
session.GetConversationId("Code").Should().Be(conv.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveCurrentConversation_RemembersConversationId_WhenOnlyDraftQueueExists()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var conv = session.EnsureCurrentConversation("Code");
|
||||
conv.Messages.Clear();
|
||||
conv.ExecutionEvents.Clear();
|
||||
conv.AgentRunHistory.Clear();
|
||||
conv.DraftQueueItems = new List<DraftQueueItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "draft-1",
|
||||
Text = "todo",
|
||||
Priority = "next",
|
||||
State = "queued",
|
||||
CreatedAt = DateTime.Now
|
||||
}
|
||||
};
|
||||
|
||||
session.SaveCurrentConversation(storage, "Code");
|
||||
|
||||
session.GetConversationId("Code").Should().Be(conv.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveCurrentConversation_UsesConversationTabToAvoidCrossTabContamination()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var conv = new ChatConversation
|
||||
{
|
||||
Tab = "Code",
|
||||
Messages = [new ChatMessage { Role = "user", Content = "code message" }]
|
||||
};
|
||||
session.CurrentConversation = conv;
|
||||
|
||||
session.SaveCurrentConversation(storage, "Chat");
|
||||
|
||||
session.GetConversationId("Code").Should().Be(conv.Id);
|
||||
session.GetConversationId("Chat").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCurrentConversation_UpdatesCurrentConversationAndRememberedId()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var conversation = new ChatConversation { Id = "conv-42", Tab = "", Title = "test title" };
|
||||
|
||||
var current = session.SetCurrentConversation("Cowork", conversation);
|
||||
|
||||
current.Tab.Should().Be("Cowork");
|
||||
session.CurrentConversation.Should().BeSameAs(conversation);
|
||||
session.GetConversationId("Cowork").Should().Be("conv-42");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetCurrentConversation_ForcesConversationTabToRequestedTab()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var conversation = new ChatConversation { Id = "conv-99", Tab = "Chat", Title = "wrong tab" };
|
||||
|
||||
var current = session.SetCurrentConversation("Code", conversation);
|
||||
|
||||
current.Tab.Should().Be("Code");
|
||||
session.GetConversationId("Code").Should().Be("conv-99");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureCurrentConversation_WhenTabDiffers_CreatesIsolatedConversation()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
session.CurrentConversation = new ChatConversation
|
||||
{
|
||||
Tab = "Chat",
|
||||
Messages = [new ChatMessage { Role = "user", Content = "chat message" }]
|
||||
};
|
||||
|
||||
var codeConversation = session.EnsureCurrentConversation("Code");
|
||||
|
||||
codeConversation.Tab.Should().Be("Code");
|
||||
codeConversation.Messages.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadOrCreateConversation_WhenRememberedIdPointsToDifferentTab_CreatesFreshConversation()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var settings = new SettingsService();
|
||||
|
||||
var chatConversation = new ChatConversation
|
||||
{
|
||||
Id = "conv-chat-tab",
|
||||
Tab = "Chat",
|
||||
Title = "chat only"
|
||||
};
|
||||
storage.Save(chatConversation);
|
||||
session.RememberConversation("Code", chatConversation.Id);
|
||||
|
||||
var loaded = session.LoadOrCreateConversation("Code", storage, settings);
|
||||
|
||||
loaded.Tab.Should().Be("Code");
|
||||
loaded.Id.Should().NotBe(chatConversation.Id);
|
||||
session.GetConversationId("Code").Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void LoadOrCreateConversation_NormalizesHistoryOrderAndCompactsSize()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var settings = new SettingsService();
|
||||
var baseTime = new DateTime(2026, 4, 3, 1, 0, 0, DateTimeKind.Local);
|
||||
|
||||
var conversation = new ChatConversation
|
||||
{
|
||||
Id = $"conv-history-normalize-{Guid.NewGuid():N}",
|
||||
Tab = "Code",
|
||||
Title = "history normalize",
|
||||
};
|
||||
|
||||
for (var i = 0; i < 420; i++)
|
||||
{
|
||||
conversation.ExecutionEvents.Add(new ChatExecutionEvent
|
||||
{
|
||||
RunId = $"run-{i % 20}",
|
||||
Type = "ToolResult",
|
||||
ToolName = "file_read",
|
||||
Summary = $"event-{i}",
|
||||
Timestamp = baseTime.AddSeconds(420 - i), // 역순 저장
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
conversation.AgentRunHistory.Add(new ChatAgentRunRecord
|
||||
{
|
||||
RunId = $"run-{i}",
|
||||
Status = i % 2 == 0 ? "completed" : "failed",
|
||||
Summary = $"summary-{i}",
|
||||
UpdatedAt = baseTime.AddMinutes(i - 20), // 오래된 순 저장
|
||||
StartedAt = baseTime.AddMinutes(i - 21),
|
||||
});
|
||||
}
|
||||
|
||||
storage.Save(conversation);
|
||||
session.RememberConversation("Code", conversation.Id);
|
||||
|
||||
var loaded = session.LoadOrCreateConversation("Code", storage, settings);
|
||||
|
||||
loaded.ExecutionEvents.Should().HaveCount(400);
|
||||
loaded.ExecutionEvents.First().Timestamp.Should().BeBefore(loaded.ExecutionEvents.Last().Timestamp);
|
||||
loaded.AgentRunHistory.Should().HaveCount(12);
|
||||
loaded.AgentRunHistory.First().RunId.Should().Be("run-19");
|
||||
loaded.AgentRunHistory.Last().RunId.Should().Be("run-8");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void LoadOrCreateConversation_NormalizesAgentRunDuplicatesByRunId()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var storage = new ChatStorageService();
|
||||
var settings = new SettingsService();
|
||||
var baseTime = new DateTime(2026, 4, 3, 1, 0, 0, DateTimeKind.Local);
|
||||
|
||||
var conversation = new ChatConversation
|
||||
{
|
||||
Id = $"conv-run-dedupe-{Guid.NewGuid():N}",
|
||||
Tab = "Code",
|
||||
Title = "run dedupe",
|
||||
AgentRunHistory =
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-a", Status = "running", Summary = "old", UpdatedAt = baseTime.AddMinutes(-2), StartedAt = baseTime.AddMinutes(-3), LastIteration = 1 },
|
||||
new ChatAgentRunRecord { RunId = "run-a", Status = "completed", Summary = "new", UpdatedAt = baseTime.AddMinutes(-1), StartedAt = baseTime.AddMinutes(-3), LastIteration = 2 },
|
||||
new ChatAgentRunRecord { RunId = "run-b", Status = "failed", Summary = "other", UpdatedAt = baseTime, StartedAt = baseTime.AddMinutes(-1), LastIteration = 1 },
|
||||
]
|
||||
};
|
||||
|
||||
storage.Save(conversation);
|
||||
session.RememberConversation("Code", conversation.Id);
|
||||
|
||||
var loaded = session.LoadOrCreateConversation("Code", storage, settings);
|
||||
|
||||
loaded.AgentRunHistory.Should().HaveCount(2);
|
||||
loaded.AgentRunHistory[0].RunId.Should().Be("run-b");
|
||||
loaded.AgentRunHistory[1].RunId.Should().Be("run-a");
|
||||
loaded.AgentRunHistory[1].Status.Should().Be("completed");
|
||||
loaded.AgentRunHistory[1].Summary.Should().Be("new");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_NormalizesLegacyAndCaseInsensitiveTabKeys()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Llm.LastActiveTab = "code";
|
||||
settings.Settings.Llm.LastConversationIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["coworkcode"] = "conv-cowork-legacy",
|
||||
["code"] = "conv-code-lower",
|
||||
["chat"] = "conv-chat-lower",
|
||||
};
|
||||
|
||||
session.Load(settings);
|
||||
|
||||
session.ActiveTab.Should().Be("Code");
|
||||
session.GetConversationId("Cowork").Should().Be("conv-cowork-legacy");
|
||||
session.GetConversationId("Code").Should().Be("conv-code-lower");
|
||||
session.GetConversationId("Chat").Should().Be("conv-chat-lower");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendMessage_FirstUserMessageUpdatesConversationTitle()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
session.AppendMessage("Chat", new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "first user request"
|
||||
}, useForTitle: true);
|
||||
session.AppendMessage("Chat", new ChatMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = "answer"
|
||||
});
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Messages.Should().HaveCount(2);
|
||||
session.CurrentConversation.Title.Should().Be("first user request");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendMessage_RemembersConversationIdForActiveTab()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
session.AppendMessage("Code", new ChatMessage
|
||||
{
|
||||
Role = "user",
|
||||
Content = "run build"
|
||||
}, useForTitle: true);
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Tab.Should().Be("Code");
|
||||
session.GetConversationId("Code").Should().Be(session.CurrentConversation.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateConversationMetadata_UpdatesCurrentConversationFields()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
session.EnsureCurrentConversation("Code");
|
||||
|
||||
session.UpdateConversationMetadata("Code", conv =>
|
||||
{
|
||||
conv.Title = "new title";
|
||||
conv.Category = ChatCategory.Product;
|
||||
conv.SystemCommand = "system prompt";
|
||||
});
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Title.Should().Be("new title");
|
||||
session.CurrentConversation.Category.Should().Be(ChatCategory.Product);
|
||||
session.CurrentConversation.SystemCommand.Should().Be("system prompt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveConversationSettings_UpdatesConversationScopedPreferences()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
session.SaveConversationSettings("Cowork", "Ask", "active", "markdown", "modern");
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Permission.Should().Be("Ask");
|
||||
session.CurrentConversation.DataUsage.Should().Be("active");
|
||||
session.CurrentConversation.OutputFormat.Should().Be("markdown");
|
||||
session.CurrentConversation.Mood.Should().Be("modern");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLastAssistantMessage_RemovesOnlyAssistantTail()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "u1" });
|
||||
session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "a1" });
|
||||
|
||||
var removed = session.RemoveLastAssistantMessage("Chat");
|
||||
|
||||
removed.Should().BeTrue();
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Messages.Should().ContainSingle();
|
||||
session.CurrentConversation.Messages[0].Role.Should().Be("user");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateUserMessageAndTrim_RewritesTailFromTargetIndex()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "u1" });
|
||||
session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "a1" });
|
||||
session.AppendMessage("Chat", new ChatMessage { Role = "user", Content = "u2" });
|
||||
session.AppendMessage("Chat", new ChatMessage { Role = "assistant", Content = "a2" });
|
||||
|
||||
var updated = session.UpdateUserMessageAndTrim("Chat", 2, "u2-edited");
|
||||
|
||||
updated.Should().BeTrue();
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Messages.Should().HaveCount(3);
|
||||
session.CurrentConversation.Messages[2].Content.Should().Be("u2-edited");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateMessageFeedback_UpdatesStoredMessageFeedback()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var message = new ChatMessage { Role = "assistant", Content = "answer" };
|
||||
session.AppendMessage("Chat", message);
|
||||
|
||||
var updated = session.UpdateMessageFeedback("Chat", message, "like");
|
||||
|
||||
updated.Should().BeTrue();
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.Messages[0].Feedback.Should().Be("like");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFreshConversation_AppliesDefaultWorkFolderOutsideChatTab()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Llm.WorkFolder = @"E:\workspace";
|
||||
|
||||
var conversation = session.CreateFreshConversation("Code", settings);
|
||||
|
||||
conversation.Tab.Should().Be("Code");
|
||||
conversation.WorkFolder.Should().Be(@"E:\workspace");
|
||||
session.CurrentConversation.Should().BeSameAs(conversation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateBranchConversation_ClonesConversationContextUpToBranchPoint()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var source = new ChatConversation
|
||||
{
|
||||
Id = "source-1",
|
||||
Title = "Main",
|
||||
Tab = "Code",
|
||||
Category = ChatCategory.Product,
|
||||
WorkFolder = @"E:\workspace",
|
||||
SystemCommand = "system",
|
||||
ConversationFailedOnlyFilter = true,
|
||||
ConversationRunningOnlyFilter = true,
|
||||
ConversationSortMode = "recent",
|
||||
AgentRunHistory =
|
||||
[
|
||||
new ChatAgentRunRecord { RunId = "run-1", Status = "completed", Summary = "done", LastIteration = 2 }
|
||||
],
|
||||
Messages =
|
||||
[
|
||||
new ChatMessage { Role = "user", Content = "u1", Timestamp = DateTime.Now.AddMinutes(-3) },
|
||||
new ChatMessage { Role = "assistant", Content = "a1", Timestamp = DateTime.Now.AddMinutes(-2), MetaKind = "meta", MetaRunId = "run-1" },
|
||||
new ChatMessage { Role = "user", Content = "u2", Timestamp = DateTime.Now.AddMinutes(-1) }
|
||||
]
|
||||
};
|
||||
|
||||
var branch = session.CreateBranchConversation(source, 1, 2, "follow-up", "context message", "run-ctx");
|
||||
|
||||
branch.ParentId.Should().Be("source-1");
|
||||
branch.Tab.Should().Be("Code");
|
||||
branch.WorkFolder.Should().Be(@"E:\workspace");
|
||||
branch.BranchLabel.Should().Contain("2");
|
||||
branch.Messages.Should().HaveCount(3);
|
||||
branch.Messages[1].MetaRunId.Should().Be("run-1");
|
||||
branch.Messages[2].MetaKind.Should().Be("branch_context");
|
||||
branch.AgentRunHistory.Should().ContainSingle();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DraftStateHelpers_SelectAndTransitionQueuedItems()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var first = session.EnqueueDraft("Chat", "first", "next");
|
||||
var second = session.EnqueueDraft("Chat", "second", "now");
|
||||
|
||||
var next = session.GetNextQueuedDraft("Chat");
|
||||
|
||||
next.Should().NotBeNull();
|
||||
next!.Id.Should().Be(second!.Id);
|
||||
session.MarkDraftRunning("Chat", second.Id).Should().BeTrue();
|
||||
session.MarkDraftFailed("Chat", second.Id, "error").Should().BeTrue();
|
||||
session.MarkDraftCompleted("Chat", first!.Id).Should().BeTrue();
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.DraftQueueItems.Should().Contain(x => x.Id == second.Id && x.State == "failed" && x.LastError == "error");
|
||||
session.CurrentConversation.DraftQueueItems.Should().Contain(x => x.Id == first.Id && x.State == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DraftStateHelpers_ResetAndRemoveDraft()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var item = session.EnqueueDraft("Chat", "queued item", "next");
|
||||
|
||||
session.MarkDraftRunning("Chat", item!.Id).Should().BeTrue();
|
||||
session.MarkDraftFailed("Chat", item.Id, "boom").Should().BeTrue();
|
||||
session.ResetDraftToQueued("Chat", item.Id).Should().BeTrue();
|
||||
session.RemoveDraft("Chat", item.Id).Should().BeTrue();
|
||||
|
||||
session.CurrentConversation.Should().NotBeNull();
|
||||
session.CurrentConversation!.DraftQueueItems.Should().NotContain(x => x.Id == item.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDraftQueueSummary_ReturnsConversationQueueSnapshot()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var first = session.EnqueueDraft("Chat", "first", "next");
|
||||
var second = session.EnqueueDraft("Chat", "second", "now");
|
||||
|
||||
session.MarkDraftRunning("Chat", second!.Id);
|
||||
session.MarkDraftCompleted("Chat", second.Id);
|
||||
|
||||
var summary = session.GetDraftQueueSummary("Chat");
|
||||
|
||||
summary.TotalCount.Should().Be(2);
|
||||
summary.QueuedCount.Should().Be(1);
|
||||
summary.CompletedCount.Should().Be(1);
|
||||
summary.NextItem.Should().NotBeNull();
|
||||
summary.NextItem!.Id.Should().Be(first!.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RememberConversation_NormalizesCoworkAliasesToSingleBucket()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
|
||||
session.RememberConversation("Cowork Code", "conv-1");
|
||||
session.RememberConversation("cowork/code", "conv-2");
|
||||
session.RememberConversation("코워크/코드", "conv-3");
|
||||
|
||||
session.GetConversationId("Cowork").Should().Be("conv-3");
|
||||
session.GetConversationId("Code").Should().BeNull();
|
||||
session.GetConversationId("Chat").Should().BeNull();
|
||||
}
|
||||
}
|
||||
95
src/AxCopilot.Tests/Services/ContextCondenserTests.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Reflection;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class ContextCondenserTests
|
||||
{
|
||||
[Fact]
|
||||
public void TruncateToolResults_PreservesMessageMetadataOnCompression()
|
||||
{
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = "{\"type\":\"tool_result\",\"output\":\"" + new string('a', 4200) + "\"}",
|
||||
Timestamp = new DateTime(2026, 4, 3, 1, 0, 0),
|
||||
MetaKind = "tool_result",
|
||||
MetaRunId = "run-1",
|
||||
Feedback = "like",
|
||||
AttachedFiles = [@"E:\sample\a.txt"],
|
||||
Images =
|
||||
[
|
||||
new ImageAttachment
|
||||
{
|
||||
FileName = "image.png",
|
||||
MimeType = "image/png",
|
||||
Base64 = "AAA"
|
||||
}
|
||||
]
|
||||
},
|
||||
new() { Role = "user", Content = "recent-1" },
|
||||
new() { Role = "assistant", Content = "recent-2" },
|
||||
new() { Role = "user", Content = "recent-3" },
|
||||
new() { Role = "assistant", Content = "recent-4" },
|
||||
new() { Role = "user", Content = "recent-5" },
|
||||
new() { Role = "assistant", Content = "recent-6" },
|
||||
};
|
||||
|
||||
var changed = InvokePrivateStatic<bool>("TruncateToolResults", messages);
|
||||
|
||||
changed.Should().BeTrue();
|
||||
messages[0].MetaKind.Should().Be("tool_result");
|
||||
messages[0].MetaRunId.Should().Be("run-1");
|
||||
messages[0].Feedback.Should().Be("like");
|
||||
messages[0].AttachedFiles.Should().ContainSingle().Which.Should().Be(@"E:\sample\a.txt");
|
||||
messages[0].Images.Should().ContainSingle();
|
||||
messages[0].Images![0].FileName.Should().Be("image.png");
|
||||
messages[0].Content.Length.Should().BeLessThan(4200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TruncateToolResults_PreservesMetadataForLongAssistantMessage()
|
||||
{
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = new string('b', 5000),
|
||||
Timestamp = new DateTime(2026, 4, 3, 1, 5, 0),
|
||||
MetaKind = "analysis",
|
||||
MetaRunId = "run-2",
|
||||
AttachedFiles = [@"E:\sample\b.txt"],
|
||||
},
|
||||
new() { Role = "user", Content = "recent-1" },
|
||||
new() { Role = "assistant", Content = "recent-2" },
|
||||
new() { Role = "user", Content = "recent-3" },
|
||||
new() { Role = "assistant", Content = "recent-4" },
|
||||
new() { Role = "user", Content = "recent-5" },
|
||||
new() { Role = "assistant", Content = "recent-6" },
|
||||
};
|
||||
|
||||
var changed = InvokePrivateStatic<bool>("TruncateToolResults", messages);
|
||||
|
||||
changed.Should().BeTrue();
|
||||
messages[0].MetaKind.Should().Be("analysis");
|
||||
messages[0].MetaRunId.Should().Be("run-2");
|
||||
messages[0].AttachedFiles.Should().ContainSingle().Which.Should().Be(@"E:\sample\b.txt");
|
||||
messages[0].Content.Length.Should().BeLessThan(5000);
|
||||
}
|
||||
|
||||
private static T InvokePrivateStatic<T>(string methodName, params object?[] arguments)
|
||||
{
|
||||
var method = typeof(ContextCondenser).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var result = method!.Invoke(null, arguments);
|
||||
result.Should().NotBeNull();
|
||||
return (T)result!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using AxCopilot.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class DraftQueueProcessorServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryStartNext_PicksPreferredDraftAndMarksItRunning()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var processor = new DraftQueueProcessorService();
|
||||
var first = session.EnqueueDraft("Chat", "first", "next");
|
||||
var second = session.EnqueueDraft("Chat", "second", "later");
|
||||
|
||||
var started = processor.TryStartNext(session, "Chat", preferredDraftId: second!.Id);
|
||||
|
||||
started.Should().NotBeNull();
|
||||
started!.Id.Should().Be(second.Id);
|
||||
session.GetDraftQueueItems("Chat").Single(x => x.Id == second.Id).State.Should().Be("running");
|
||||
session.GetDraftQueueItems("Chat").Single(x => x.Id == first!.Id).State.Should().Be("queued");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleFailure_SchedulesRetryBeforeRetryLimit()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var processor = new DraftQueueProcessorService();
|
||||
var item = session.EnqueueDraft("Chat", "retry me", "next");
|
||||
processor.TryStartNext(session, "Chat", preferredDraftId: item!.Id);
|
||||
|
||||
var handled = processor.HandleFailure(session, "Chat", item.Id, "boom", cancelled: false, maxAutoRetries: 3);
|
||||
|
||||
handled.Should().BeTrue();
|
||||
var stored = session.GetDraftQueueItems("Chat").Single(x => x.Id == item.Id);
|
||||
stored.State.Should().Be("queued");
|
||||
stored.NextRetryAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromoteReadyBlockedItems_ClearsExpiredRetryWindow()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var processor = new DraftQueueProcessorService();
|
||||
var item = session.EnqueueDraft("Chat", "retry me", "next");
|
||||
processor.TryStartNext(session, "Chat", preferredDraftId: item!.Id);
|
||||
processor.HandleFailure(session, "Chat", item.Id, "boom", cancelled: false, maxAutoRetries: 3);
|
||||
session.CurrentConversation!.DraftQueueItems[0].NextRetryAt = DateTime.Now.AddSeconds(-1);
|
||||
|
||||
var promoted = processor.PromoteReadyBlockedItems(session, "Chat");
|
||||
|
||||
promoted.Should().Be(1);
|
||||
session.CurrentConversation!.DraftQueueItems[0].NextRetryAt.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearCompleted_RemovesCompletedItems()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var processor = new DraftQueueProcessorService();
|
||||
var item = session.EnqueueDraft("Chat", "done item", "next");
|
||||
processor.TryStartNext(session, "Chat", preferredDraftId: item!.Id);
|
||||
processor.Complete(session, "Chat", item.Id);
|
||||
|
||||
var removed = processor.ClearCompleted(session, "Chat");
|
||||
|
||||
removed.Should().Be(1);
|
||||
session.GetDraftQueueItems("Chat").Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryStartNext_AndHandleFailure_RecordQueueTasks()
|
||||
{
|
||||
var session = new ChatSessionStateService();
|
||||
var processor = new DraftQueueProcessorService();
|
||||
var taskRuns = new TaskRunService();
|
||||
var item = session.EnqueueDraft("Chat", "retry me", "next");
|
||||
|
||||
processor.TryStartNext(session, "Chat", preferredDraftId: item!.Id, taskRuns: taskRuns);
|
||||
processor.HandleFailure(session, "Chat", item.Id, "boom", cancelled: false, maxAutoRetries: 3, taskRuns: taskRuns);
|
||||
|
||||
taskRuns.ActiveTasks.Should().BeEmpty();
|
||||
taskRuns.RecentTasks.Should().ContainSingle(t => t.Kind == "queue" && t.Status == "blocked");
|
||||
}
|
||||
}
|
||||
124
src/AxCopilot.Tests/Services/DraftQueueServiceTests.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class DraftQueueServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetNextQueuedItem_UsesPriorityThenCreatedAt()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var later = service.CreateItem("later item", "later");
|
||||
later.CreatedAt = DateTime.Now.AddMinutes(-3);
|
||||
var next = service.CreateItem("next item", "next");
|
||||
next.CreatedAt = DateTime.Now.AddMinutes(-2);
|
||||
var now = service.CreateItem("now item", "now");
|
||||
now.CreatedAt = DateTime.Now.AddMinutes(-1);
|
||||
|
||||
var selected = service.GetNextQueuedItem(new[] { later, next, now });
|
||||
|
||||
selected.Should().NotBeNull();
|
||||
selected!.Text.Should().Be("now item");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkRunningAndCompleted_UpdateState()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var item = service.CreateItem("task");
|
||||
|
||||
service.MarkRunning(item).Should().BeTrue();
|
||||
item.State.Should().Be("running");
|
||||
item.AttemptCount.Should().Be(1);
|
||||
|
||||
service.MarkCompleted(item).Should().BeTrue();
|
||||
item.State.Should().Be("completed");
|
||||
item.LastError.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkFailed_SetsErrorAndKeepsAttemptHistory()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var item = service.CreateItem("task");
|
||||
|
||||
service.MarkRunning(item);
|
||||
service.MarkFailed(item, "boom").Should().BeTrue();
|
||||
|
||||
item.State.Should().Be("failed");
|
||||
item.LastError.Should().Be("boom");
|
||||
item.AttemptCount.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetToQueued_ClearsErrorAndReturnsQueuedState()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var item = service.CreateItem("task");
|
||||
|
||||
service.MarkRunning(item);
|
||||
service.MarkFailed(item, "boom");
|
||||
service.ResetToQueued(item).Should().BeTrue();
|
||||
|
||||
item.State.Should().Be("queued");
|
||||
item.LastError.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSummary_ReturnsCountsAndNextItem()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var queued = service.CreateItem("queued", "next");
|
||||
var running = service.CreateItem("running", "now");
|
||||
var failed = service.CreateItem("failed", "later");
|
||||
|
||||
service.MarkRunning(running);
|
||||
service.MarkFailed(failed, "boom");
|
||||
|
||||
var summary = service.GetSummary(new[] { queued, running, failed });
|
||||
|
||||
summary.TotalCount.Should().Be(3);
|
||||
summary.QueuedCount.Should().Be(1);
|
||||
summary.RunningCount.Should().Be(1);
|
||||
summary.FailedCount.Should().Be(1);
|
||||
summary.CompletedCount.Should().Be(0);
|
||||
summary.NextItem.Should().NotBeNull();
|
||||
summary.NextItem!.Text.Should().Be("queued");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScheduleRetry_BlocksItemUntilRetryTime()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var item = service.CreateItem("task");
|
||||
|
||||
service.MarkRunning(item);
|
||||
service.ScheduleRetry(item, "temporary");
|
||||
|
||||
item.State.Should().Be("queued");
|
||||
item.LastError.Should().Be("temporary");
|
||||
item.NextRetryAt.Should().NotBeNull();
|
||||
service.CanRunNow(item, item.NextRetryAt!.Value.AddSeconds(-1)).Should().BeFalse();
|
||||
service.CanRunNow(item, item.NextRetryAt!.Value.AddSeconds(1)).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSummary_IncludesBlockedItemsAndNextReadyAt()
|
||||
{
|
||||
var service = new DraftQueueService();
|
||||
var blocked = service.CreateItem("blocked");
|
||||
blocked.NextRetryAt = DateTime.Now.AddMinutes(2);
|
||||
var ready = service.CreateItem("ready");
|
||||
|
||||
var summary = service.GetSummary(new[] { blocked, ready });
|
||||
|
||||
summary.QueuedCount.Should().Be(1);
|
||||
summary.BlockedCount.Should().Be(1);
|
||||
summary.NextReadyAt.Should().NotBeNull();
|
||||
summary.NextItem.Should().NotBeNull();
|
||||
summary.NextItem!.Text.Should().Be("ready");
|
||||
}
|
||||
}
|
||||
68
src/AxCopilot.Tests/Services/LlmOperationModeTests.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class LlmOperationModeTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendAsync_InternalMode_BlocksExternalGemini()
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.OperationMode = OperationModePolicy.InternalMode;
|
||||
settings.Settings.Llm.Service = "gemini";
|
||||
settings.Settings.Llm.Model = "gemini-2.5-flash";
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
|
||||
var action = async () => await llm.SendAsync(
|
||||
[new ChatMessage { Role = "user", Content = "test" }]);
|
||||
|
||||
var ex = await action.Should().ThrowAsync<InvalidOperationException>();
|
||||
ex.Which.Message.Should().Contain("사내 모드");
|
||||
ex.Which.Message.Should().Contain("외부 LLM 호출이 차단");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendWithToolsAsync_InternalMode_BlocksExternalClaude()
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.OperationMode = OperationModePolicy.InternalMode;
|
||||
settings.Settings.Llm.Service = "claude";
|
||||
settings.Settings.Llm.Model = "claude-sonnet-4-5";
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
var tools = new List<IAgentTool> { new FileReadTool() };
|
||||
|
||||
var action = async () => await llm.SendWithToolsAsync(
|
||||
[new ChatMessage { Role = "user", Content = "test" }],
|
||||
tools);
|
||||
|
||||
var ex = await action.Should().ThrowAsync<InvalidOperationException>();
|
||||
ex.Which.Message.Should().Contain("사내 모드");
|
||||
ex.Which.Message.Should().Contain("Claude");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_ExternalMode_DoesNotUseInternalModeBlockMessage()
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.OperationMode = OperationModePolicy.ExternalMode;
|
||||
settings.Settings.Llm.Service = "gemini";
|
||||
settings.Settings.Llm.Model = "gemini-2.5-flash";
|
||||
settings.Settings.Llm.GeminiApiKey = "";
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
|
||||
var action = async () => await llm.SendAsync(
|
||||
[new ChatMessage { Role = "user", Content = "test" }]);
|
||||
|
||||
var ex = await action.Should().ThrowAsync<InvalidOperationException>();
|
||||
ex.Which.Message.Should().NotContain("사내 모드에서는 외부 LLM 호출이 차단");
|
||||
ex.Which.Message.Should().Contain("API 키");
|
||||
}
|
||||
}
|
||||
|
||||
111
src/AxCopilot.Tests/Services/LlmRuntimeOverrideTests.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class LlmRuntimeOverrideTests
|
||||
{
|
||||
[Fact]
|
||||
public void PushInferenceOverride_PopInferenceOverride_RestoresPreviousState()
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Llm.Service = "ollama";
|
||||
settings.Settings.Llm.Model = "base-model";
|
||||
settings.Settings.Llm.Temperature = 0.7;
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
|
||||
llm.PushRouteOverride("gemini", "gemini-2.5-pro");
|
||||
llm.PushInferenceOverride(temperature: 0.2, reasoningEffort: "high");
|
||||
|
||||
llm.GetCurrentModelInfo().service.Should().Be("gemini");
|
||||
llm.GetCurrentModelInfo().model.Should().Be("gemini-2.5-pro");
|
||||
InvokePrivate<double>(llm, "ResolveTemperature").Should().Be(0.2);
|
||||
InvokePrivate<string?>(llm, "ResolveReasoningEffort").Should().Be("high");
|
||||
|
||||
llm.PopInferenceOverride();
|
||||
llm.GetCurrentModelInfo().service.Should().Be("gemini");
|
||||
llm.GetCurrentModelInfo().model.Should().Be("gemini-2.5-pro");
|
||||
InvokePrivate<double>(llm, "ResolveTemperature").Should().Be(0.7);
|
||||
InvokePrivate<string?>(llm, "ResolveReasoningEffort").Should().BeNull();
|
||||
|
||||
llm.ClearRouteOverride();
|
||||
llm.GetCurrentModelInfo().service.Should().Be("ollama");
|
||||
llm.GetCurrentModelInfo().model.Should().Be("base-model");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AgentLoop_ResolveSkillRuntimeOverrides_MapsModelAndEffort()
|
||||
{
|
||||
var settings = new SettingsService();
|
||||
settings.Settings.Llm.Service = "ollama";
|
||||
settings.Settings.Llm.Model = "base-model";
|
||||
settings.Settings.Llm.RegisteredModels =
|
||||
[
|
||||
new RegisteredModel
|
||||
{
|
||||
Alias = "gpt-5.4",
|
||||
EncryptedModelName = "gpt-5.4",
|
||||
Service = "vllm",
|
||||
}
|
||||
];
|
||||
|
||||
using var llm = new LlmService(settings);
|
||||
var loop = new AgentLoopService(llm, ToolRegistry.CreateDefault(), settings);
|
||||
var messages = new List<ChatMessage>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Role = "system",
|
||||
Content = """
|
||||
[Skill Runtime Policy]
|
||||
- preferred_model: gpt-5.4
|
||||
- reasoning_effort: high
|
||||
- execution_context: fork
|
||||
- allowed_tools: Read, process
|
||||
- hook_names: lint-pre, verify-post
|
||||
- hook_filters: lint-pre@pre@file_edit, verify-post@post@*
|
||||
"""
|
||||
},
|
||||
new() { Role = "user", Content = "test" }
|
||||
};
|
||||
|
||||
var method = typeof(AgentLoopService).GetMethod(
|
||||
"ResolveSkillRuntimeOverrides",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var result = method!.Invoke(loop, [messages]);
|
||||
result.Should().NotBeNull();
|
||||
|
||||
var resultType = result!.GetType();
|
||||
resultType.GetProperty("Service")!.GetValue(result)!.Should().Be("vllm");
|
||||
resultType.GetProperty("Model")!.GetValue(result)!.Should().Be("gpt-5.4");
|
||||
resultType.GetProperty("ReasoningEffort")!.GetValue(result)!.Should().Be("high");
|
||||
resultType.GetProperty("Temperature")!.GetValue(result)!.Should().Be(0.2);
|
||||
resultType.GetProperty("RequireForkExecution")!.GetValue(result)!.Should().Be(true);
|
||||
|
||||
var allowedSet = (IReadOnlySet<string>)resultType.GetProperty("AllowedToolNames")!.GetValue(result)!;
|
||||
allowedSet.Should().Contain("file_read");
|
||||
allowedSet.Should().Contain("process");
|
||||
|
||||
var hookSet = (IReadOnlySet<string>)resultType.GetProperty("HookNames")!.GetValue(result)!;
|
||||
hookSet.Should().Contain("lint-pre");
|
||||
hookSet.Should().Contain("verify-post");
|
||||
|
||||
var filters = (IReadOnlyList<object>)resultType.GetProperty("HookFilters")!.GetValue(result)!;
|
||||
filters.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static T InvokePrivate<T>(object instance, string methodName)
|
||||
{
|
||||
var method = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
method.Should().NotBeNull();
|
||||
return (T)method!.Invoke(instance, null)!;
|
||||
}
|
||||
}
|
||||
62
src/AxCopilot.Tests/Services/OperationModePolicyTests.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class OperationModePolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void Normalize_DefaultsToInternalWhenMissingOrUnknown()
|
||||
{
|
||||
OperationModePolicy.Normalize(null).Should().Be(OperationModePolicy.InternalMode);
|
||||
OperationModePolicy.Normalize("").Should().Be(OperationModePolicy.InternalMode);
|
||||
OperationModePolicy.Normalize("unknown").Should().Be(OperationModePolicy.InternalMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Normalize_MapsExternalMode()
|
||||
{
|
||||
OperationModePolicy.Normalize("external").Should().Be(OperationModePolicy.ExternalMode);
|
||||
OperationModePolicy.Normalize("EXTERNAL").Should().Be(OperationModePolicy.ExternalMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsBlockedAgentToolInInternalMode_BlocksExternalHttpAndUrlOpen()
|
||||
{
|
||||
OperationModePolicy.IsBlockedAgentToolInInternalMode("http_tool", "https://example.com").Should().BeTrue();
|
||||
OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", "https://example.com").Should().BeTrue();
|
||||
OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", @"E:\work\report.html").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AgentContext_CheckToolPermissionAsync_BlocksRestrictedToolsInInternalMode()
|
||||
{
|
||||
var context = new AgentContext
|
||||
{
|
||||
OperationMode = OperationModePolicy.InternalMode,
|
||||
Permission = "Auto"
|
||||
};
|
||||
|
||||
var blocked = await context.CheckToolPermissionAsync("http_tool", "https://example.com");
|
||||
var allowed = await context.CheckToolPermissionAsync("file_read", @"E:\work\a.txt");
|
||||
|
||||
blocked.Should().BeFalse();
|
||||
allowed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AgentContext_CheckToolPermissionAsync_AllowsRestrictedToolsInExternalMode()
|
||||
{
|
||||
var context = new AgentContext
|
||||
{
|
||||
OperationMode = OperationModePolicy.ExternalMode,
|
||||
Permission = "Auto"
|
||||
};
|
||||
|
||||
var allowed = await context.CheckToolPermissionAsync("http_tool", "https://example.com");
|
||||
allowed.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
278
src/AxCopilot.Tests/Services/SettingsServiceTests.cs
Normal file
@@ -0,0 +1,278 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using AxCopilot.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class SettingsServiceTests
|
||||
{
|
||||
// ─── AppSettings 기본값 ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_DefaultHotkey_IsAltSpace()
|
||||
{
|
||||
new AppSettings().Hotkey.Should().Be("Alt+Space");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LauncherSettings_DefaultMaxResults_IsSeven()
|
||||
{
|
||||
new LauncherSettings().MaxResults.Should().Be(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LauncherSettings_DefaultTheme_IsSystem()
|
||||
{
|
||||
new LauncherSettings().Theme.Should().Be("system");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LauncherSettings_DefaultOpacity_IsValid()
|
||||
{
|
||||
var opacity = new LauncherSettings().Opacity;
|
||||
opacity.Should().BeInRange(0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_DefaultMonitorMismatch_IsWarn()
|
||||
{
|
||||
new AppSettings().MonitorMismatch.Should().Be("warn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_DefaultCleanupPeriodDays_IsThirty()
|
||||
{
|
||||
new AppSettings().CleanupPeriodDays.Should().Be(30);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_DefaultIndexPaths_NotEmpty()
|
||||
{
|
||||
new AppSettings().IndexPaths.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
// ─── LauncherSettings 테마 ───────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData("system")]
|
||||
[InlineData("dark")]
|
||||
[InlineData("light")]
|
||||
public void LauncherSettings_Theme_AcceptsValidValues(string theme)
|
||||
{
|
||||
var settings = new LauncherSettings { Theme = theme };
|
||||
settings.Theme.Should().Be(theme);
|
||||
}
|
||||
|
||||
// ─── JSON 직렬화 라운드트립 ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_Serialization_PreservesHotkey()
|
||||
{
|
||||
var original = new AppSettings { Hotkey = "Ctrl+Space" };
|
||||
var restored = RoundTrip(original);
|
||||
restored.Hotkey.Should().Be("Ctrl+Space");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_Serialization_PreservesTheme()
|
||||
{
|
||||
var original = new AppSettings
|
||||
{
|
||||
Launcher = new LauncherSettings { Theme = "dark" }
|
||||
};
|
||||
var restored = RoundTrip(original);
|
||||
restored.Launcher.Theme.Should().Be("dark");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_Serialization_PreservesMaxResults()
|
||||
{
|
||||
var original = new AppSettings
|
||||
{
|
||||
Launcher = new LauncherSettings { MaxResults = 15 }
|
||||
};
|
||||
var restored = RoundTrip(original);
|
||||
restored.Launcher.MaxResults.Should().Be(15);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_Serialization_PreservesAliases()
|
||||
{
|
||||
var original = new AppSettings
|
||||
{
|
||||
Aliases =
|
||||
[
|
||||
new() { Key = "@test", Type = "url", Target = "https://example.com" }
|
||||
]
|
||||
};
|
||||
var restored = RoundTrip(original);
|
||||
restored.Aliases.Should().HaveCount(1);
|
||||
restored.Aliases[0].Key.Should().Be("@test");
|
||||
restored.Aliases[0].Target.Should().Be("https://example.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppSettings_Serialization_PreservesCleanupPeriodDays()
|
||||
{
|
||||
var original = new AppSettings { CleanupPeriodDays = 14 };
|
||||
|
||||
var restored = RoundTrip(original);
|
||||
|
||||
restored.CleanupPeriodDays.Should().Be(14);
|
||||
}
|
||||
|
||||
// ─── AliasEntry ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AliasEntry_DefaultShowWindow_IsFalse()
|
||||
{
|
||||
new AliasEntry().ShowWindow.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("url")]
|
||||
[InlineData("folder")]
|
||||
[InlineData("app")]
|
||||
[InlineData("batch")]
|
||||
[InlineData("api")]
|
||||
[InlineData("clipboard")]
|
||||
public void AliasEntry_Type_AcceptsAllTypes(string type)
|
||||
{
|
||||
var entry = new AliasEntry { Type = type };
|
||||
entry.Type.Should().Be(type);
|
||||
}
|
||||
|
||||
// ─── WorkspaceProfile / WindowSnapshot ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void WorkspaceProfile_DefaultWindows_IsEmpty()
|
||||
{
|
||||
new WorkspaceProfile().Windows.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowSnapshot_DefaultShowCmd_IsNormal()
|
||||
{
|
||||
new WindowSnapshot().ShowCmd.Should().Be("Normal");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowSnapshot_DefaultMonitor_IsZero()
|
||||
{
|
||||
new WindowSnapshot().Monitor.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowRect_DefaultValues_AreZero()
|
||||
{
|
||||
var rect = new WindowRect();
|
||||
rect.X.Should().Be(0);
|
||||
rect.Y.Should().Be(0);
|
||||
rect.Width.Should().Be(0);
|
||||
rect.Height.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorkspaceProfile_Serialization_RoundTrip()
|
||||
{
|
||||
var profile = new WorkspaceProfile
|
||||
{
|
||||
Name = "작업 프로필",
|
||||
Windows =
|
||||
[
|
||||
new() { Exe = "notepad.exe", ShowCmd = "Maximized", Monitor = 1 }
|
||||
]
|
||||
};
|
||||
var json = JsonSerializer.Serialize(profile, JsonOptions);
|
||||
var restored = JsonSerializer.Deserialize<WorkspaceProfile>(json, JsonOptions)!;
|
||||
|
||||
restored.Name.Should().Be("작업 프로필");
|
||||
restored.Windows.Should().HaveCount(1);
|
||||
restored.Windows[0].Exe.Should().Be("notepad.exe");
|
||||
restored.Windows[0].ShowCmd.Should().Be("Maximized");
|
||||
restored.Windows[0].Monitor.Should().Be(1);
|
||||
}
|
||||
|
||||
// ─── ClipboardTransformer ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ClipboardTransformer_DefaultTimeout_IsFiveSeconds()
|
||||
{
|
||||
new ClipboardTransformer().Timeout.Should().Be(5000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClipboardTransformer_DefaultType_IsRegex()
|
||||
{
|
||||
new ClipboardTransformer().Type.Should().Be("regex");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeLlmThresholds_ClampsConfiguredValues()
|
||||
{
|
||||
var llm = new LlmSettings
|
||||
{
|
||||
ReadOnlySignatureLoopThreshold = 1,
|
||||
ReadOnlyStagnationThreshold = 99,
|
||||
NoProgressRecoveryThreshold = 2,
|
||||
NoProgressAbortThreshold = 200,
|
||||
NoProgressRecoveryMaxRetries = 9,
|
||||
ToolExecutionTimeoutMs = 1000
|
||||
};
|
||||
|
||||
InvokeNormalizeLlmThresholds(llm);
|
||||
|
||||
llm.ReadOnlySignatureLoopThreshold.Should().Be(2);
|
||||
llm.ReadOnlyStagnationThreshold.Should().Be(20);
|
||||
llm.NoProgressRecoveryThreshold.Should().Be(4);
|
||||
llm.NoProgressAbortThreshold.Should().Be(50);
|
||||
llm.NoProgressRecoveryMaxRetries.Should().Be(5);
|
||||
llm.ToolExecutionTimeoutMs.Should().Be(5000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeLlmThresholds_PreservesZeroAsUnset()
|
||||
{
|
||||
var llm = new LlmSettings
|
||||
{
|
||||
ReadOnlySignatureLoopThreshold = 0,
|
||||
ReadOnlyStagnationThreshold = 0,
|
||||
NoProgressRecoveryThreshold = 0,
|
||||
NoProgressAbortThreshold = 0,
|
||||
NoProgressRecoveryMaxRetries = 0,
|
||||
ToolExecutionTimeoutMs = 0
|
||||
};
|
||||
|
||||
InvokeNormalizeLlmThresholds(llm);
|
||||
|
||||
llm.ReadOnlySignatureLoopThreshold.Should().Be(0);
|
||||
llm.ReadOnlyStagnationThreshold.Should().Be(0);
|
||||
llm.NoProgressRecoveryThreshold.Should().Be(0);
|
||||
llm.NoProgressAbortThreshold.Should().Be(0);
|
||||
llm.NoProgressRecoveryMaxRetries.Should().Be(0);
|
||||
llm.ToolExecutionTimeoutMs.Should().Be(0);
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private static AppSettings RoundTrip(AppSettings original)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(original, JsonOptions);
|
||||
return JsonSerializer.Deserialize<AppSettings>(json, JsonOptions)!;
|
||||
}
|
||||
|
||||
private static void InvokeNormalizeLlmThresholds(LlmSettings llm)
|
||||
{
|
||||
var method = typeof(AxCopilot.Services.SettingsService)
|
||||
.GetMethod("NormalizeLlmThresholds", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
method!.Invoke(null, [llm]);
|
||||
}
|
||||
}
|
||||
165
src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class SkillServiceRuntimePolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildRuntimeDirective_ReturnsEmpty_WhenNoRuntimeMetadata()
|
||||
{
|
||||
var skill = new SkillDefinition
|
||||
{
|
||||
Name = "plain-skill",
|
||||
SystemPrompt = "do work"
|
||||
};
|
||||
|
||||
var directive = SkillService.BuildRuntimeDirective(skill);
|
||||
|
||||
directive.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildRuntimeDirective_ContainsForkAgentEffortAndModelHints()
|
||||
{
|
||||
var skill = new SkillDefinition
|
||||
{
|
||||
Name = "advanced-skill",
|
||||
ExecutionContext = "fork",
|
||||
Agent = "worker",
|
||||
Effort = "high",
|
||||
Model = "gpt-5.4",
|
||||
DisableModelInvocation = true,
|
||||
AllowedTools = "Read, process, WebFetch",
|
||||
Hooks = "lint-pre, verify-post",
|
||||
HookFilters = "lint-pre@pre@file_edit, verify-post@post@*"
|
||||
};
|
||||
|
||||
var directive = SkillService.BuildRuntimeDirective(skill);
|
||||
|
||||
directive.Should().Contain("[Skill Runtime Policy]");
|
||||
directive.Should().Contain("execution_context: fork");
|
||||
directive.Should().Contain("preferred_agent: worker");
|
||||
directive.Should().Contain("reasoning_effort: high");
|
||||
directive.Should().Contain("preferred_model: gpt-5.4");
|
||||
directive.Should().Contain("allowed_tools: file_read, http_tool, process");
|
||||
directive.Should().Contain("hook_names: lint-pre, verify-post");
|
||||
directive.Should().Contain("hook_filters: lint-pre@pre@file_edit, verify-post@post@*");
|
||||
directive.Should().Contain("disable_model_invocation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseSkillFile_HooksMapAndList_AreNormalizedIntoHooksField()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-hooks-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var skillPath = Path.Combine(tempDir, "SKILL.md");
|
||||
try
|
||||
{
|
||||
var content = """
|
||||
---
|
||||
name: hook-skill
|
||||
hooks:
|
||||
pre: lint-pre, verify-pre
|
||||
post:
|
||||
- verify-post
|
||||
- report-post
|
||||
---
|
||||
|
||||
body
|
||||
""";
|
||||
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.Hooks.Should().Contain("lint-pre");
|
||||
parsed.Hooks.Should().Contain("verify-pre");
|
||||
parsed.Hooks.Should().Contain("verify-post");
|
||||
parsed.Hooks.Should().Contain("report-post");
|
||||
parsed.HookFilters.Should().Contain("lint-pre@pre@*");
|
||||
parsed.HookFilters.Should().Contain("verify-pre@pre@*");
|
||||
parsed.HookFilters.Should().Contain("*@post@*");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseSkillFile_NestedHooksMap_PreservesToolTimingFilters()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-hooks-nested-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var skillPath = Path.Combine(tempDir, "SKILL.md");
|
||||
try
|
||||
{
|
||||
var content = """
|
||||
---
|
||||
name: nested-hook-skill
|
||||
hooks:
|
||||
file_edit:
|
||||
pre:
|
||||
- lint-pre
|
||||
post: verify-post
|
||||
---
|
||||
|
||||
body
|
||||
""";
|
||||
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.Hooks.Should().Contain("lint-pre");
|
||||
parsed.Hooks.Should().Contain("verify-post");
|
||||
parsed.HookFilters.Should().Contain("lint-pre@pre@file_edit");
|
||||
parsed.HookFilters.Should().Contain("verify-post@post@file_edit");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseSkillFile_SampleFlag_SetsIsSample()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-sample-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var skillPath = Path.Combine(tempDir, "SKILL.md");
|
||||
try
|
||||
{
|
||||
var content = """
|
||||
---
|
||||
name: sample-skill
|
||||
sample: true
|
||||
---
|
||||
|
||||
body
|
||||
""";
|
||||
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||
|
||||
var method = typeof(SkillService).GetMethod("ParseSkillFile", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var parsed = method!.Invoke(null, [skillPath]) as SkillDefinition;
|
||||
parsed.Should().NotBeNull();
|
||||
parsed!.IsSample.Should().BeTrue();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
276
src/AxCopilot.Tests/Services/TaskRunServiceTests.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class TaskRunServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void StartOrUpdate_UsesUnderlyingStore()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
|
||||
service.StartOrUpdate("agent:1", "agent", "main", "thinking");
|
||||
service.StartOrUpdate("agent:1", "agent", "main", "done", "running", @"E:\a.txt");
|
||||
|
||||
service.ActiveTasks.Should().ContainSingle();
|
||||
service.ActiveTasks[0].Summary.Should().Be("done");
|
||||
service.ActiveTasks[0].FilePath.Should().Be(@"E:\a.txt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteAndRestoreRecent_ExposeStoreState()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
service.StartOrUpdate("tool:1", "tool", "file_read", "reading");
|
||||
|
||||
service.Complete("tool:1", "done");
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().ContainSingle();
|
||||
|
||||
service.RestoreRecent(
|
||||
[
|
||||
new TaskRunStore.TaskRun
|
||||
{
|
||||
Id = "agent:2",
|
||||
Kind = "agent",
|
||||
Title = "main",
|
||||
Summary = "restored",
|
||||
Status = "completed",
|
||||
UpdatedAt = DateTime.Now
|
||||
}
|
||||
]);
|
||||
|
||||
service.RecentTasks.Should().ContainSingle();
|
||||
service.RecentTasks[0].Id.Should().Be("agent:2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Changed_FiresWhenUnderlyingStoreChanges()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var count = 0;
|
||||
service.Changed += () => count++;
|
||||
|
||||
service.StartOrUpdate("tool:1", "tool", "grep", "searching");
|
||||
service.Complete("tool:1", "done");
|
||||
|
||||
count.Should().BeGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyAgentEvent_TracksPermissionAndToolLifecycle()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
|
||||
service.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.PermissionRequest,
|
||||
ToolName = "file_write",
|
||||
Summary = "ask"
|
||||
});
|
||||
service.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.ToolCall,
|
||||
ToolName = "file_read",
|
||||
Summary = "reading"
|
||||
});
|
||||
service.ApplyAgentEvent(new AgentEvent
|
||||
{
|
||||
RunId = "run-1",
|
||||
Type = AgentEventType.ToolResult,
|
||||
ToolName = "file_read",
|
||||
Summary = "done",
|
||||
Success = true
|
||||
});
|
||||
|
||||
service.ActiveTasks.Should().ContainSingle(t => t.Kind == "permission");
|
||||
service.RecentTasks.Should().ContainSingle(t => t.Kind == "tool" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplySubAgentStatus_TracksSubAgentLifecycle()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
|
||||
service.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-1",
|
||||
Task = "scan",
|
||||
Status = SubAgentRunStatus.Started,
|
||||
Summary = "started"
|
||||
});
|
||||
service.ApplySubAgentStatus(new SubAgentStatusEvent
|
||||
{
|
||||
Id = "worker-1",
|
||||
Task = "scan",
|
||||
Status = SubAgentRunStatus.Completed,
|
||||
Summary = "completed"
|
||||
});
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().ContainSingle(t => t.Kind == "subagent" && t.Status == "completed");
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "background" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExplicitLifecycleApis_TrackPermissionHookAndBackgroundTasks()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
|
||||
service.StartPermissionRequest("run-2", "file_write", "ask");
|
||||
service.CompletePermissionRequest("run-2", "file_write", "granted", true);
|
||||
service.RecordHookResult("run-2", "file_write", "pre hook ok", true);
|
||||
service.StartBackgroundRun("worker-2", "worker-2", "running");
|
||||
service.CompleteBackgroundRun("worker-2", "worker-2", "done", true);
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "permission" && t.Status == "completed");
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "hook" && t.Status == "completed");
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "background" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueueLifecycleApis_TrackQueueTasks()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
|
||||
service.StartQueueRun("Chat", "draft-1", "first draft");
|
||||
service.CompleteQueueRun("Chat", "draft-1", "retry wait", "blocked");
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().ContainSingle(t => t.Kind == "queue" && t.Status == "blocked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreRecentFromExecutionEvents_BuildsRecentTaskHistory()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
service.RestoreRecentFromExecutionEvents(
|
||||
[
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddMinutes(-3), RunId = "run-1", Type = "ToolResult", ToolName = "file_read", Summary = "read", Success = true },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddMinutes(-2), RunId = "run-1", Type = "PermissionDenied", ToolName = "file_write", Summary = "deny", Success = false },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddMinutes(-1), RunId = "run-1", Type = "Complete", Summary = "done", Success = true },
|
||||
]);
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().HaveCount(3);
|
||||
service.RecentTasks[0].Kind.Should().Be("agent");
|
||||
service.RecentTasks[1].Kind.Should().Be("permission");
|
||||
service.RecentTasks[2].Kind.Should().Be("tool");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void RestoreRecentFromExecutionEvents_PrefersTerminalEventsWhenTimestampsEqual()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
service.RestoreRecentFromExecutionEvents(
|
||||
[
|
||||
new Models.ChatExecutionEvent { Timestamp = now, RunId = "run-1", Type = "ToolCall", ToolName = "file_read", Summary = "call", Success = true, Iteration = 1 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now, RunId = "run-1", Type = "ToolResult", ToolName = "file_read", Summary = "done", Success = false, Iteration = 2 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now, RunId = "run-1", Type = "PermissionRequest", ToolName = "file_write", Summary = "ask", Success = true, Iteration = 1 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now, RunId = "run-1", Type = "PermissionDenied", ToolName = "file_write", Summary = "deny", Success = false, Iteration = 2 },
|
||||
]);
|
||||
|
||||
service.RecentTasks.Should().HaveCount(2);
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "tool" && t.Status == "failed" && t.Summary == "done");
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "permission" && t.Status == "failed" && t.Summary == "deny");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void RestoreRecentFromExecutionEvents_RebuildsActiveTasksFromNonTerminalEvents()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
service.RestoreRecentFromExecutionEvents(
|
||||
[
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-3), RunId = "run-2", Type = "Thinking", Summary = "thinking", Iteration = 1 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-2), RunId = "run-2", Type = "ToolCall", ToolName = "file_read", Summary = "call", Iteration = 2 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-1), RunId = "run-2", Type = "PermissionRequest", ToolName = "file_write", Summary = "ask", Iteration = 3 },
|
||||
]);
|
||||
|
||||
service.ActiveTasks.Should().HaveCount(3);
|
||||
service.ActiveTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "running");
|
||||
service.ActiveTasks.Should().Contain(t => t.Kind == "tool" && t.Status == "running");
|
||||
service.ActiveTasks.Should().Contain(t => t.Kind == "permission" && t.Status == "waiting");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreRecentFromExecutionEvents_RemovesActiveTaskWhenTerminalEventAppears()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
service.RestoreRecentFromExecutionEvents(
|
||||
[
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-2), RunId = "run-3", Type = "ToolCall", ToolName = "file_read", Summary = "call", Iteration = 1 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-1), RunId = "run-3", Type = "ToolResult", ToolName = "file_read", Summary = "done", Success = true, Iteration = 2 },
|
||||
]);
|
||||
|
||||
service.ActiveTasks.Should().NotContain(t => t.Kind == "tool" && t.Title == "file_read");
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "tool" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Suite", "ReplayStability")]
|
||||
public void RestoreRecentFromExecutionEvents_CompleteClearsDanglingRunScopedActiveTasks()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
service.RestoreRecentFromExecutionEvents(
|
||||
[
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-3), RunId = "run-4", Type = "ToolCall", ToolName = "file_edit", Summary = "call", Iteration = 1 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-2), RunId = "run-4", Type = "PermissionRequest", ToolName = "file_edit", Summary = "ask", Iteration = 2 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-1), RunId = "run-4", Type = "Complete", Summary = "done", Iteration = 3, Success = true },
|
||||
]);
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreRecentFromExecutionEvents_RunLevelErrorClearsDanglingRunScopedActiveTasks()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
var now = DateTime.Now;
|
||||
|
||||
service.RestoreRecentFromExecutionEvents(
|
||||
[
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-3), RunId = "run-5", Type = "ToolCall", ToolName = "file_write", Summary = "call", Iteration = 1 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-2), RunId = "run-5", Type = "PermissionRequest", ToolName = "file_write", Summary = "ask", Iteration = 2 },
|
||||
new Models.ChatExecutionEvent { Timestamp = now.AddSeconds(-1), RunId = "run-5", Type = "Error", Summary = "fatal", Iteration = 3, Success = false },
|
||||
]);
|
||||
|
||||
service.ActiveTasks.Should().BeEmpty();
|
||||
service.RecentTasks.Should().Contain(t => t.Kind == "agent" && t.Status == "failed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSummary_ReturnsActiveAndPendingPermissionCounts()
|
||||
{
|
||||
var service = new TaskRunService();
|
||||
service.StartOrUpdate("agent:1", "agent", "main", "thinking");
|
||||
service.StartOrUpdate("permission:1:file_write", "permission", "file_write 권한", "ask", "waiting");
|
||||
service.Complete("agent:1", "done", "completed");
|
||||
|
||||
var summary = service.GetSummary();
|
||||
|
||||
summary.ActiveCount.Should().Be(1);
|
||||
summary.PendingPermissionCount.Should().Be(1);
|
||||
summary.LatestRecentTask.Should().NotBeNull();
|
||||
summary.LatestRecentTask!.Id.Should().Be("agent:1");
|
||||
}
|
||||
}
|
||||
87
src/AxCopilot.Tests/Services/TaskRunStoreTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using AxCopilot.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class TaskRunStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void Upsert_UpdatesExistingTaskWithoutDuplicating()
|
||||
{
|
||||
var store = new TaskRunStore();
|
||||
|
||||
store.Upsert("tool:1", "tool", "file_read", "first", "running");
|
||||
store.Upsert("tool:1", "tool", "file_read", "updated", "running", @"E:\a.txt");
|
||||
|
||||
store.ActiveTasks.Should().HaveCount(1);
|
||||
store.ActiveTasks[0].Summary.Should().Be("updated");
|
||||
store.ActiveTasks[0].FilePath.Should().Be(@"E:\a.txt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Complete_MovesTaskToRecent()
|
||||
{
|
||||
var store = new TaskRunStore();
|
||||
store.Upsert("agent:1", "agent", "main", "working", "running");
|
||||
|
||||
store.Complete("agent:1", "done", "completed");
|
||||
|
||||
store.ActiveTasks.Should().BeEmpty();
|
||||
store.RecentTasks.Should().ContainSingle();
|
||||
store.RecentTasks[0].Summary.Should().Be("done");
|
||||
store.RecentTasks[0].Status.Should().Be("completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompleteByPrefix_MovesMatchingTasksOnly()
|
||||
{
|
||||
var store = new TaskRunStore();
|
||||
store.Upsert("tool:run1:file_read", "tool", "file_read", "reading", "running");
|
||||
store.Upsert("tool:run1:grep", "tool", "grep", "searching", "running");
|
||||
store.Upsert("agent:run1", "agent", "main", "planning", "running");
|
||||
|
||||
store.CompleteByPrefix("tool:run1:", "tool batch done", "completed");
|
||||
|
||||
store.ActiveTasks.Should().ContainSingle(t => t.Id == "agent:run1");
|
||||
store.RecentTasks.Should().HaveCount(2);
|
||||
store.RecentTasks.Should().OnlyContain(t => t.Summary == "tool batch done");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Changed_FiresWhenStoreChanges()
|
||||
{
|
||||
var store = new TaskRunStore();
|
||||
var count = 0;
|
||||
store.Changed += () => count++;
|
||||
|
||||
store.Upsert("agent:1", "agent", "main", "thinking", "running");
|
||||
store.Complete("agent:1", "done", "completed");
|
||||
|
||||
count.Should().BeGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreRecent_ReplacesRecentSnapshotAndClearsActive()
|
||||
{
|
||||
var store = new TaskRunStore();
|
||||
store.Upsert("agent:1", "agent", "main", "thinking", "running");
|
||||
|
||||
store.RestoreRecent(
|
||||
[
|
||||
new TaskRunStore.TaskRun
|
||||
{
|
||||
Id = "tool:1",
|
||||
Kind = "tool",
|
||||
Title = "file_read",
|
||||
Summary = "done",
|
||||
Status = "completed",
|
||||
UpdatedAt = DateTime.Now,
|
||||
}
|
||||
]);
|
||||
|
||||
store.ActiveTasks.Should().BeEmpty();
|
||||
store.RecentTasks.Should().ContainSingle();
|
||||
store.RecentTasks[0].Id.Should().Be("tool:1");
|
||||
}
|
||||
}
|
||||
44
src/AxCopilot.Tests/Services/TaskTypePolicyTests.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class TaskTypePolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void FromTaskType_Bugfix_ProvidesExpectedFields()
|
||||
{
|
||||
var policy = TaskTypePolicy.FromTaskType("bugfix");
|
||||
|
||||
policy.TaskType.Should().Be("bugfix");
|
||||
policy.GuidanceMessage.Should().Contain("bug-fix");
|
||||
policy.FailurePatternFocus.Should().Contain("재현 조건");
|
||||
policy.FollowUpTaskLine.Should().Contain("작업 유형: bugfix");
|
||||
policy.FinalReportTaskLine.Should().Contain("버그 수정");
|
||||
policy.IsReviewTask.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromTaskType_Review_SetsReviewFlag()
|
||||
{
|
||||
var policy = TaskTypePolicy.FromTaskType("review");
|
||||
|
||||
policy.TaskType.Should().Be("review");
|
||||
policy.IsReviewTask.Should().BeTrue();
|
||||
policy.GuidanceMessage.Should().Contain("review task");
|
||||
policy.FailureInvestigationTaskLine.Should().Contain("리뷰에서 지적된 위험");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromTaskType_Unknown_FallsBackToGeneral()
|
||||
{
|
||||
var policy = TaskTypePolicy.FromTaskType("unknown_type");
|
||||
|
||||
policy.TaskType.Should().Be("general");
|
||||
policy.GuidanceMessage.Should().Contain("cautious analyze");
|
||||
policy.FollowUpTaskLine.Should().BeEmpty();
|
||||
policy.FailureInvestigationTaskLine.Should().BeEmpty();
|
||||
policy.FinalReportTaskLine.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
78
src/AxCopilot.Tests/Services/TextFileCodecTests.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using AxCopilot.Services.Agent;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public sealed class TextFileCodecTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReadAllTextAsync_DetectsUtf8WithoutBom()
|
||||
{
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
var content = "한글 UTF8 테스트\nline2";
|
||||
await TextFileCodec.WriteAllTextAsync(path, content, TextFileCodec.Utf8NoBom);
|
||||
|
||||
var result = await TextFileCodec.ReadAllTextAsync(path);
|
||||
|
||||
result.Text.Should().Be(content);
|
||||
result.Encoding.CodePage.Should().Be(Encoding.UTF8.CodePage);
|
||||
result.HasBom.Should().BeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(path); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveWriteEncoding_PreservesUtf8Bom()
|
||||
{
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
var bomUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
|
||||
await TextFileCodec.WriteAllTextAsync(path, "first", bomUtf8);
|
||||
|
||||
var read = await TextFileCodec.ReadAllTextAsync(path);
|
||||
var writeEnc = TextFileCodec.ResolveWriteEncoding(read.Encoding, read.HasBom);
|
||||
await TextFileCodec.WriteAllTextAsync(path, "second", writeEnc);
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(path);
|
||||
bytes.Length.Should().BeGreaterThan(3);
|
||||
bytes[0].Should().Be(0xEF);
|
||||
bytes[1].Should().Be(0xBB);
|
||||
bytes[2].Should().Be(0xBF);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(path); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAllTextAsync_ReadsLegacyKoreanEncoding()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
var cp949 = Encoding.GetEncoding(949);
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
var source = "가나다 테스트";
|
||||
await File.WriteAllBytesAsync(path, cp949.GetBytes(source));
|
||||
|
||||
var result = await TextFileCodec.ReadAllTextAsync(path);
|
||||
|
||||
result.Text.Should().Be(source);
|
||||
result.HasBom.Should().BeFalse();
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(path); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 519 B |
|
After Width: | Height: | Size: 615 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 247 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 555 B |
|
After Width: | Height: | Size: 488 B |
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/Assets/icon.ico
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.SDK.dll
Normal file
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.SDK.pdb
Normal file
@@ -0,0 +1,941 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"AxCopilot.Tests/1.0.0": {
|
||||
"dependencies": {
|
||||
"AxCopilot": "1.7.2",
|
||||
"FluentAssertions": "6.12.0",
|
||||
"Microsoft.NET.Test.Sdk": "17.11.0",
|
||||
"xunit": "2.9.0",
|
||||
"Microsoft.Web.WebView2.Core": "1.0.2903.40",
|
||||
"Microsoft.Web.WebView2.WinForms": "1.0.2903.40",
|
||||
"Microsoft.Web.WebView2.Wpf": "1.0.2903.40"
|
||||
},
|
||||
"runtime": {
|
||||
"AxCopilot.Tests.dll": {}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.2.0": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml.Framework": "3.2.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/DocumentFormat.OpenXml.dll": {
|
||||
"assemblyVersion": "3.2.0.0",
|
||||
"fileVersion": "3.2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.2.0": {
|
||||
"dependencies": {
|
||||
"System.IO.Packaging": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/DocumentFormat.OpenXml.Framework.dll": {
|
||||
"assemblyVersion": "3.2.0.0",
|
||||
"fileVersion": "3.2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FluentAssertions/6.12.0": {
|
||||
"runtime": {
|
||||
"lib/net6.0/FluentAssertions.dll": {
|
||||
"assemblyVersion": "6.12.0.0",
|
||||
"fileVersion": "6.12.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Markdig/0.37.0": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Markdig.dll": {
|
||||
"assemblyVersion": "0.37.0.0",
|
||||
"fileVersion": "0.37.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeCoverage/17.11.0": {
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.1/Microsoft.VisualStudio.CodeCoverage.Shim.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.424.36701"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite/8.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "8.0.0",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/8.0.0": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.Data.Sqlite.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.23.53103"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.NET.Test.Sdk/17.11.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.CodeCoverage": "17.11.0",
|
||||
"Microsoft.TestPlatform.TestHost": "17.11.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel/17.11.0": {
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.1/Microsoft.TestPlatform.CoreUtilities.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
},
|
||||
"lib/netcoreapp3.1/Microsoft.TestPlatform.PlatformAbstractions.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
},
|
||||
"lib/netcoreapp3.1/Microsoft.VisualStudio.TestPlatform.ObjectModel.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"lib/netcoreapp3.1/cs/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "cs"
|
||||
},
|
||||
"lib/netcoreapp3.1/cs/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "cs"
|
||||
},
|
||||
"lib/netcoreapp3.1/de/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "de"
|
||||
},
|
||||
"lib/netcoreapp3.1/de/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "de"
|
||||
},
|
||||
"lib/netcoreapp3.1/es/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "es"
|
||||
},
|
||||
"lib/netcoreapp3.1/es/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "es"
|
||||
},
|
||||
"lib/netcoreapp3.1/fr/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "fr"
|
||||
},
|
||||
"lib/netcoreapp3.1/fr/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "fr"
|
||||
},
|
||||
"lib/netcoreapp3.1/it/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "it"
|
||||
},
|
||||
"lib/netcoreapp3.1/it/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "it"
|
||||
},
|
||||
"lib/netcoreapp3.1/ja/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "ja"
|
||||
},
|
||||
"lib/netcoreapp3.1/ja/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "ja"
|
||||
},
|
||||
"lib/netcoreapp3.1/ko/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "ko"
|
||||
},
|
||||
"lib/netcoreapp3.1/ko/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "ko"
|
||||
},
|
||||
"lib/netcoreapp3.1/pl/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "pl"
|
||||
},
|
||||
"lib/netcoreapp3.1/pl/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "pl"
|
||||
},
|
||||
"lib/netcoreapp3.1/pt-BR/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "pt-BR"
|
||||
},
|
||||
"lib/netcoreapp3.1/pt-BR/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "pt-BR"
|
||||
},
|
||||
"lib/netcoreapp3.1/ru/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "ru"
|
||||
},
|
||||
"lib/netcoreapp3.1/ru/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "ru"
|
||||
},
|
||||
"lib/netcoreapp3.1/tr/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "tr"
|
||||
},
|
||||
"lib/netcoreapp3.1/tr/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "tr"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "zh-Hans"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "zh-Hans"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.TestPlatform.CoreUtilities.resources.dll": {
|
||||
"locale": "zh-Hant"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.VisualStudio.TestPlatform.ObjectModel.resources.dll": {
|
||||
"locale": "zh-Hant"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.TestPlatform.TestHost/17.11.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.TestPlatform.ObjectModel": "17.11.0",
|
||||
"Newtonsoft.Json": "13.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netcoreapp3.1/Microsoft.TestPlatform.CommunicationUtilities.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
},
|
||||
"lib/netcoreapp3.1/Microsoft.TestPlatform.CrossPlatEngine.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
},
|
||||
"lib/netcoreapp3.1/Microsoft.TestPlatform.Utilities.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
},
|
||||
"lib/netcoreapp3.1/Microsoft.VisualStudio.TestPlatform.Common.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
},
|
||||
"lib/netcoreapp3.1/testhost.dll": {
|
||||
"assemblyVersion": "15.0.0.0",
|
||||
"fileVersion": "17.1100.24.41901"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"lib/netcoreapp3.1/cs/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "cs"
|
||||
},
|
||||
"lib/netcoreapp3.1/cs/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "cs"
|
||||
},
|
||||
"lib/netcoreapp3.1/cs/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "cs"
|
||||
},
|
||||
"lib/netcoreapp3.1/de/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "de"
|
||||
},
|
||||
"lib/netcoreapp3.1/de/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "de"
|
||||
},
|
||||
"lib/netcoreapp3.1/de/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "de"
|
||||
},
|
||||
"lib/netcoreapp3.1/es/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "es"
|
||||
},
|
||||
"lib/netcoreapp3.1/es/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "es"
|
||||
},
|
||||
"lib/netcoreapp3.1/es/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "es"
|
||||
},
|
||||
"lib/netcoreapp3.1/fr/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "fr"
|
||||
},
|
||||
"lib/netcoreapp3.1/fr/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "fr"
|
||||
},
|
||||
"lib/netcoreapp3.1/fr/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "fr"
|
||||
},
|
||||
"lib/netcoreapp3.1/it/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "it"
|
||||
},
|
||||
"lib/netcoreapp3.1/it/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "it"
|
||||
},
|
||||
"lib/netcoreapp3.1/it/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "it"
|
||||
},
|
||||
"lib/netcoreapp3.1/ja/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "ja"
|
||||
},
|
||||
"lib/netcoreapp3.1/ja/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "ja"
|
||||
},
|
||||
"lib/netcoreapp3.1/ja/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "ja"
|
||||
},
|
||||
"lib/netcoreapp3.1/ko/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "ko"
|
||||
},
|
||||
"lib/netcoreapp3.1/ko/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "ko"
|
||||
},
|
||||
"lib/netcoreapp3.1/ko/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "ko"
|
||||
},
|
||||
"lib/netcoreapp3.1/pl/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "pl"
|
||||
},
|
||||
"lib/netcoreapp3.1/pl/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "pl"
|
||||
},
|
||||
"lib/netcoreapp3.1/pl/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "pl"
|
||||
},
|
||||
"lib/netcoreapp3.1/pt-BR/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "pt-BR"
|
||||
},
|
||||
"lib/netcoreapp3.1/pt-BR/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "pt-BR"
|
||||
},
|
||||
"lib/netcoreapp3.1/pt-BR/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "pt-BR"
|
||||
},
|
||||
"lib/netcoreapp3.1/ru/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "ru"
|
||||
},
|
||||
"lib/netcoreapp3.1/ru/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "ru"
|
||||
},
|
||||
"lib/netcoreapp3.1/ru/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "ru"
|
||||
},
|
||||
"lib/netcoreapp3.1/tr/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "tr"
|
||||
},
|
||||
"lib/netcoreapp3.1/tr/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "tr"
|
||||
},
|
||||
"lib/netcoreapp3.1/tr/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "tr"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "zh-Hans"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "zh-Hans"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hans/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "zh-Hans"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.TestPlatform.CommunicationUtilities.resources.dll": {
|
||||
"locale": "zh-Hant"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.TestPlatform.CrossPlatEngine.resources.dll": {
|
||||
"locale": "zh-Hant"
|
||||
},
|
||||
"lib/netcoreapp3.1/zh-Hant/Microsoft.VisualStudio.TestPlatform.Common.resources.dll": {
|
||||
"locale": "zh-Hant"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2/1.0.2903.40": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/win-arm64/native/WebView2Loader.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
},
|
||||
"runtimes/win-x64/native/WebView2Loader.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
},
|
||||
"runtimes/win-x86/native/WebView2Loader.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Newtonsoft.Json/13.0.1": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Newtonsoft.Json.dll": {
|
||||
"assemblyVersion": "13.0.0.0",
|
||||
"fileVersion": "13.0.1.25517"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.6": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.core.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||
"runtimeTargets": {
|
||||
"runtimes/browser-wasm/nativeassets/net8.0/e_sqlite3.a": {
|
||||
"rid": "browser-wasm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-arm/native/libe_sqlite3.so": {
|
||||
"rid": "linux-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-arm64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-armel/native/libe_sqlite3.so": {
|
||||
"rid": "linux-armel",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-mips64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-mips64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-arm/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-arm64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-musl-x64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-musl-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-ppc64le/native/libe_sqlite3.so": {
|
||||
"rid": "linux-ppc64le",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-s390x/native/libe_sqlite3.so": {
|
||||
"rid": "linux-s390x",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x64/native/libe_sqlite3.so": {
|
||||
"rid": "linux-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/linux-x86/native/libe_sqlite3.so": {
|
||||
"rid": "linux-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/maccatalyst-arm64/native/libe_sqlite3.dylib": {
|
||||
"rid": "maccatalyst-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/maccatalyst-x64/native/libe_sqlite3.dylib": {
|
||||
"rid": "maccatalyst-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-arm64/native/libe_sqlite3.dylib": {
|
||||
"rid": "osx-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/osx-x64/native/libe_sqlite3.dylib": {
|
||||
"rid": "osx-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-arm/native/e_sqlite3.dll": {
|
||||
"rid": "win-arm",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-arm64/native/e_sqlite3.dll": {
|
||||
"rid": "win-arm64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x64/native/e_sqlite3.dll": {
|
||||
"rid": "win-x64",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
},
|
||||
"runtimes/win-x86/native/e_sqlite3.dll": {
|
||||
"rid": "win-x86",
|
||||
"assetType": "native",
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0-windows7.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.EventLog/8.0.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/System.Diagnostics.EventLog.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/win/lib/net8.0/System.Diagnostics.EventLog.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/System.IO.Packaging.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.ServiceProcess.ServiceController/8.0.1": {
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/System.ServiceProcess.ServiceController.dll": {
|
||||
"assemblyVersion": "8.0.0.1",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
},
|
||||
"runtimeTargets": {
|
||||
"runtimes/win/lib/net8.0/System.ServiceProcess.ServiceController.dll": {
|
||||
"rid": "win",
|
||||
"assetType": "runtime",
|
||||
"assemblyVersion": "8.0.0.1",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Fonts": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokenization": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokens": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Core/1.7.0-custom-5": {
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Core.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Fonts/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokenization": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokens": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Fonts.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Tokenization/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokens": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Tokenization.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Tokens/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Tokens.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xunit/2.9.0": {
|
||||
"dependencies": {
|
||||
"xunit.assert": "2.9.0",
|
||||
"xunit.core": "2.9.0"
|
||||
}
|
||||
},
|
||||
"xunit.abstractions/2.0.3": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/xunit.abstractions.dll": {
|
||||
"assemblyVersion": "2.0.0.0",
|
||||
"fileVersion": "2.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xunit.assert/2.9.0": {
|
||||
"runtime": {
|
||||
"lib/net6.0/xunit.assert.dll": {
|
||||
"assemblyVersion": "2.9.0.0",
|
||||
"fileVersion": "2.9.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xunit.core/2.9.0": {
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "2.9.0",
|
||||
"xunit.extensibility.execution": "2.9.0"
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.core/2.9.0": {
|
||||
"dependencies": {
|
||||
"xunit.abstractions": "2.0.3"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard1.1/xunit.core.dll": {
|
||||
"assemblyVersion": "2.9.0.0",
|
||||
"fileVersion": "2.9.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"xunit.extensibility.execution/2.9.0": {
|
||||
"dependencies": {
|
||||
"xunit.extensibility.core": "2.9.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard1.1/xunit.execution.dotnet.dll": {
|
||||
"assemblyVersion": "2.9.0.0",
|
||||
"fileVersion": "2.9.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AxCopilot/1.7.2": {
|
||||
"dependencies": {
|
||||
"AxCopilot.SDK": "1.0.0",
|
||||
"DocumentFormat.OpenXml": "3.2.0",
|
||||
"Markdig": "0.37.0",
|
||||
"Microsoft.Data.Sqlite": "8.0.0",
|
||||
"Microsoft.Web.WebView2": "1.0.2903.40",
|
||||
"System.ServiceProcess.ServiceController": "8.0.1",
|
||||
"UglyToad.PdfPig": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"AxCopilot.dll": {
|
||||
"assemblyVersion": "1.7.2.0",
|
||||
"fileVersion": "1.7.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AxCopilot.SDK/1.0.0": {
|
||||
"runtime": {
|
||||
"AxCopilot.SDK.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2.Core/1.0.2903.40": {
|
||||
"runtime": {
|
||||
"Microsoft.Web.WebView2.Core.dll": {
|
||||
"assemblyVersion": "1.0.2903.40",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2.WinForms/1.0.2903.40": {
|
||||
"runtime": {
|
||||
"Microsoft.Web.WebView2.WinForms.dll": {
|
||||
"assemblyVersion": "1.0.2903.40",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2.Wpf/1.0.2903.40": {
|
||||
"runtime": {
|
||||
"Microsoft.Web.WebView2.Wpf.dll": {
|
||||
"assemblyVersion": "1.0.2903.40",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"AxCopilot.Tests/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.2.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-eDBT9G0sAWUvjgE8l8E5bGCFXgxCZXIecQ8dqUnj2PyxyMR5eBmLahqRRw3Q7uSKM3cKbysaL2mEY0JJbEEOEA==",
|
||||
"path": "documentformat.openxml/3.2.0",
|
||||
"hashPath": "documentformat.openxml.3.2.0.nupkg.sha512"
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.2.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-e1neOKqRnSHUom4JQEorAoZ67aiJOp6+Xzsu0fc6IYfFcgQn6roo+w6i2w//N2u/5ilEfvLr35bNO9zaIN7r7g==",
|
||||
"path": "documentformat.openxml.framework/3.2.0",
|
||||
"hashPath": "documentformat.openxml.framework.3.2.0.nupkg.sha512"
|
||||
},
|
||||
"FluentAssertions/6.12.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ZXhHT2YwP9lajrwSKbLlFqsmCCvFJMoRSK9t7sImfnCyd0OB3MhgxdoMcVqxbq1iyxD6mD2fiackWmBb7ayiXQ==",
|
||||
"path": "fluentassertions/6.12.0",
|
||||
"hashPath": "fluentassertions.6.12.0.nupkg.sha512"
|
||||
},
|
||||
"Markdig/0.37.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-biiu4MTPFjW55qw6v5Aphtj0MjDLJ14x8ndZwkJUHIeqvaSGKeqhLY7S7Vu/S3k7/c9KwhhnaCDP9hdFNUhcNA==",
|
||||
"path": "markdig/0.37.0",
|
||||
"hashPath": "markdig.0.37.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.CodeCoverage/17.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-QKcOSuw7MZG4XiQ+pCj+Ib6amOwoRDEO7e3DbxqXeOPXSnfyGXYoZQI8I140s1mKQVn1Vh+c5WlKvCvlgMovpg==",
|
||||
"path": "microsoft.codecoverage/17.11.0",
|
||||
"hashPath": "microsoft.codecoverage.17.11.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Data.Sqlite/8.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-H+iC5IvkCCKSNHXzL3JARvDn7VpkvuJM91KVB89sKjeTF/KX/BocNNh93ZJtX5MCQKb/z4yVKgkU2sVIq+xKfg==",
|
||||
"path": "microsoft.data.sqlite/8.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.8.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/8.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pujbzfszX7jAl7oTbHhqx7pxd9jibeyHHl8zy1gd55XMaKWjDtc5XhhNYwQnrwWYCInNdVoArbaaAvLgW7TwuA==",
|
||||
"path": "microsoft.data.sqlite.core/8.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.core.8.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.NET.Test.Sdk/17.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-fH7P0LihMXgnlNLtrXGetHd30aQcD+YrSbWXbCPBnrypdRApPgNqd/TgncTlSVY1bbLYdnvpBgts2dcnK37GzA==",
|
||||
"path": "microsoft.net.test.sdk/17.11.0",
|
||||
"hashPath": "microsoft.net.test.sdk.17.11.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.TestPlatform.ObjectModel/17.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-PU+CC1yRzbR0IllrtdILaeep7WP5OIrvmWrvCMqG3jB1h4F6Ur7CYHl6ENbDVXPzEvygXh0GWbTyrbjfvgTpAg==",
|
||||
"path": "microsoft.testplatform.objectmodel/17.11.0",
|
||||
"hashPath": "microsoft.testplatform.objectmodel.17.11.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.TestPlatform.TestHost/17.11.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KMzJO3dm3+9W8JRQ3IDviu0v7uXP5Lgii6TuxMc5m8ynaqcGnn7Y18cMb5AsP2xp59uUHO474WZrssxBdb8ZxQ==",
|
||||
"path": "microsoft.testplatform.testhost/17.11.0",
|
||||
"hashPath": "microsoft.testplatform.testhost.17.11.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Web.WebView2/1.0.2903.40": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==",
|
||||
"path": "microsoft.web.webview2/1.0.2903.40",
|
||||
"hashPath": "microsoft.web.webview2.1.0.2903.40.nupkg.sha512"
|
||||
},
|
||||
"Newtonsoft.Json/13.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==",
|
||||
"path": "newtonsoft.json/13.0.1",
|
||||
"hashPath": "newtonsoft.json.13.0.1.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
|
||||
"path": "sqlitepclraw.bundle_e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
|
||||
"path": "sqlitepclraw.core/2.1.6",
|
||||
"hashPath": "sqlitepclraw.core.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==",
|
||||
"path": "sqlitepclraw.lib.e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
|
||||
"path": "sqlitepclraw.provider.e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"System.Diagnostics.EventLog/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-n1ZP7NM2Gkn/MgD8+eOT5MulMj6wfeQMNS2Pizvq5GHCZfjlFMXV2irQlQmJhwA2VABC57M0auudO89Iu2uRLg==",
|
||||
"path": "system.diagnostics.eventlog/8.0.1",
|
||||
"hashPath": "system.diagnostics.eventlog.8.0.1.nupkg.sha512"
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==",
|
||||
"path": "system.io.packaging/8.0.1",
|
||||
"hashPath": "system.io.packaging.8.0.1.nupkg.sha512"
|
||||
},
|
||||
"System.ServiceProcess.ServiceController/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-02I0BXo1kmMBgw03E8Hu4K6nTqur4wpQdcDZrndczPzY2fEoGvlinE35AWbyzLZ2h2IksEZ6an4tVt3hi9j1oA==",
|
||||
"path": "system.serviceprocess.servicecontroller/8.0.1",
|
||||
"hashPath": "system.serviceprocess.servicecontroller.8.0.1.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-mddnoBg+XV5YZJg+lp/LlXQ9NY9/oV/MoNjLbbLHw0uTymfyuinVePQB4ff/ELRv3s6n0G7h8q3Ycb3KYg+hgQ==",
|
||||
"path": "uglytoad.pdfpig/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Core/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-bChQUAYApM6/vgBis0+fBTZyAVqjXdqshjZDCgI3dgwUplfLJxXRrnkCOdNj0a6JNcF32R4aLpnGpTc9QmmVmg==",
|
||||
"path": "uglytoad.pdfpig.core/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.core.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Fonts/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Z6SBBAIL8wRkJNhXGYaz0CrHnNrNeuNtmwRbBtQUA1b3TDhRQppOmHCIuhjb6Vu/Rirp6FIOtzAU1lXsGik90w==",
|
||||
"path": "uglytoad.pdfpig.fonts/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.fonts.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Tokenization/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-U8VVH7VJjv6czP7qWyzDq6CRaiJQe7/sESUCL8H3kiEa3zi0l9TonIKlD/YidQ5DlgTumracii6zjLyKPEFKwA==",
|
||||
"path": "uglytoad.pdfpig.tokenization/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.tokenization.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Tokens/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-m/j5RVfL4eF/OwX6ASprzK+yzD3l7xdgQ7zQPgENhjxfuXD+hj6FSeZlmxSTt9ywvWcTCjGKAILl9XTK9iQgCQ==",
|
||||
"path": "uglytoad.pdfpig.tokens/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.tokens.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"xunit/2.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-PtU3rZ0ThdmdJqTbK7GkgFf6iBaCR6Q0uvJHznID+XEYk2v6O/b7sRxqnbi3B2gRDXxjTqMkVNayzwsqsFUxRw==",
|
||||
"path": "xunit/2.9.0",
|
||||
"hashPath": "xunit.2.9.0.nupkg.sha512"
|
||||
},
|
||||
"xunit.abstractions/2.0.3": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==",
|
||||
"path": "xunit.abstractions/2.0.3",
|
||||
"hashPath": "xunit.abstractions.2.0.3.nupkg.sha512"
|
||||
},
|
||||
"xunit.assert/2.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Z/1pyia//860wEYTKn6Q5dmgikJdRjgE4t5AoxJkK8oTmidzPLEPG574kmm7LFkMLbH6Frwmgb750kcyR+hwoA==",
|
||||
"path": "xunit.assert/2.9.0",
|
||||
"hashPath": "xunit.assert.2.9.0.nupkg.sha512"
|
||||
},
|
||||
"xunit.core/2.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-uRaop9tZsZMCaUS4AfbSPGYHtvywWnm8XXFNUqII7ShWyDBgdchY6gyDNgO4AK1Lv/1NNW61Zq63CsDV6oH6Jg==",
|
||||
"path": "xunit.core/2.9.0",
|
||||
"hashPath": "xunit.core.2.9.0.nupkg.sha512"
|
||||
},
|
||||
"xunit.extensibility.core/2.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-zjDEUSxsr6UNij4gIwCgMqQox+oLDPRZ+mubwWLci+SssPBFQD1xeRR4SvgBuXqbE0QXCJ/STVTp+lxiB5NLVA==",
|
||||
"path": "xunit.extensibility.core/2.9.0",
|
||||
"hashPath": "xunit.extensibility.core.2.9.0.nupkg.sha512"
|
||||
},
|
||||
"xunit.extensibility.execution/2.9.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-5ZTQZvmPLlBw6QzCOwM0KnMsZw6eGjbmC176QHZlcbQoMhGIeGcYzYwn5w9yXxf+4phtplMuVqTpTbFDQh2bqQ==",
|
||||
"path": "xunit.extensibility.execution/2.9.0",
|
||||
"hashPath": "xunit.extensibility.execution.2.9.0.nupkg.sha512"
|
||||
},
|
||||
"AxCopilot/1.7.2": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"AxCopilot.SDK/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Web.WebView2.Core/1.0.2903.40": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Web.WebView2.WinForms/1.0.2903.40": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Web.WebView2.Wpf/1.0.2903.40": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.Tests.dll
Normal file
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.Tests.pdb
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
|
||||
}
|
||||
}
|
||||
}
|
||||
391
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.deps.json
Normal file
@@ -0,0 +1,391 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0/win-x64",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {},
|
||||
".NETCoreApp,Version=v8.0/win-x64": {
|
||||
"AxCopilot/1.7.2": {
|
||||
"dependencies": {
|
||||
"AxCopilot.SDK": "1.0.0",
|
||||
"DocumentFormat.OpenXml": "3.2.0",
|
||||
"Markdig": "0.37.0",
|
||||
"Microsoft.Data.Sqlite": "8.0.0",
|
||||
"Microsoft.Web.WebView2": "1.0.2903.40",
|
||||
"System.ServiceProcess.ServiceController": "8.0.1",
|
||||
"UglyToad.PdfPig": "1.7.0-custom-5",
|
||||
"Microsoft.Web.WebView2.Core": "1.0.2903.40",
|
||||
"Microsoft.Web.WebView2.WinForms": "1.0.2903.40",
|
||||
"Microsoft.Web.WebView2.Wpf": "1.0.2903.40"
|
||||
},
|
||||
"runtime": {
|
||||
"AxCopilot.dll": {}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.2.0": {
|
||||
"dependencies": {
|
||||
"DocumentFormat.OpenXml.Framework": "3.2.0"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/DocumentFormat.OpenXml.dll": {
|
||||
"assemblyVersion": "3.2.0.0",
|
||||
"fileVersion": "3.2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.2.0": {
|
||||
"dependencies": {
|
||||
"System.IO.Packaging": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/DocumentFormat.OpenXml.Framework.dll": {
|
||||
"assemblyVersion": "3.2.0.0",
|
||||
"fileVersion": "3.2.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Markdig/0.37.0": {
|
||||
"runtime": {
|
||||
"lib/net8.0/Markdig.dll": {
|
||||
"assemblyVersion": "0.37.0.0",
|
||||
"fileVersion": "0.37.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite/8.0.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.Data.Sqlite.Core": "8.0.0",
|
||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
|
||||
}
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/8.0.0": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.Data.Sqlite.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.23.53103"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2/1.0.2903.40": {
|
||||
"native": {
|
||||
"runtimes/win-x64/native/WebView2Loader.dll": {
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
|
||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.batteries_v2.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.6": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/SQLitePCLRaw.core.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||
"native": {
|
||||
"runtimes/win-x64/native/e_sqlite3.dll": {
|
||||
"fileVersion": "0.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.core": "2.1.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0-windows7.0/SQLitePCLRaw.provider.e_sqlite3.dll": {
|
||||
"assemblyVersion": "2.1.6.2060",
|
||||
"fileVersion": "2.1.6.2060"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.EventLog/8.0.1": {
|
||||
"runtime": {
|
||||
"runtimes/win/lib/net8.0/System.Diagnostics.EventLog.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"runtime": {
|
||||
"lib/net8.0/System.IO.Packaging.dll": {
|
||||
"assemblyVersion": "8.0.0.0",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
},
|
||||
"System.ServiceProcess.ServiceController/8.0.1": {
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "8.0.1"
|
||||
},
|
||||
"runtime": {
|
||||
"runtimes/win/lib/net8.0/System.ServiceProcess.ServiceController.dll": {
|
||||
"assemblyVersion": "8.0.0.1",
|
||||
"fileVersion": "8.0.1024.46610"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Fonts": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokenization": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokens": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Core/1.7.0-custom-5": {
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Core.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Fonts/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokenization": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokens": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Fonts.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Tokenization/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5",
|
||||
"UglyToad.PdfPig.Tokens": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Tokenization.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UglyToad.PdfPig.Tokens/1.7.0-custom-5": {
|
||||
"dependencies": {
|
||||
"UglyToad.PdfPig.Core": "1.7.0-custom-5"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/UglyToad.PdfPig.Tokens.dll": {
|
||||
"assemblyVersion": "0.1.8.0",
|
||||
"fileVersion": "0.1.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AxCopilot.SDK/1.0.0": {
|
||||
"runtime": {
|
||||
"AxCopilot.SDK.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2.Core/1.0.2903.40": {
|
||||
"runtime": {
|
||||
"Microsoft.Web.WebView2.Core.dll": {
|
||||
"assemblyVersion": "1.0.2903.40",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2.WinForms/1.0.2903.40": {
|
||||
"runtime": {
|
||||
"Microsoft.Web.WebView2.WinForms.dll": {
|
||||
"assemblyVersion": "1.0.2903.40",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Web.WebView2.Wpf/1.0.2903.40": {
|
||||
"runtime": {
|
||||
"Microsoft.Web.WebView2.Wpf.dll": {
|
||||
"assemblyVersion": "1.0.2903.40",
|
||||
"fileVersion": "1.0.2903.40"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"AxCopilot/1.7.2": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DocumentFormat.OpenXml/3.2.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-eDBT9G0sAWUvjgE8l8E5bGCFXgxCZXIecQ8dqUnj2PyxyMR5eBmLahqRRw3Q7uSKM3cKbysaL2mEY0JJbEEOEA==",
|
||||
"path": "documentformat.openxml/3.2.0",
|
||||
"hashPath": "documentformat.openxml.3.2.0.nupkg.sha512"
|
||||
},
|
||||
"DocumentFormat.OpenXml.Framework/3.2.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-e1neOKqRnSHUom4JQEorAoZ67aiJOp6+Xzsu0fc6IYfFcgQn6roo+w6i2w//N2u/5ilEfvLr35bNO9zaIN7r7g==",
|
||||
"path": "documentformat.openxml.framework/3.2.0",
|
||||
"hashPath": "documentformat.openxml.framework.3.2.0.nupkg.sha512"
|
||||
},
|
||||
"Markdig/0.37.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-biiu4MTPFjW55qw6v5Aphtj0MjDLJ14x8ndZwkJUHIeqvaSGKeqhLY7S7Vu/S3k7/c9KwhhnaCDP9hdFNUhcNA==",
|
||||
"path": "markdig/0.37.0",
|
||||
"hashPath": "markdig.0.37.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Data.Sqlite/8.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-H+iC5IvkCCKSNHXzL3JARvDn7VpkvuJM91KVB89sKjeTF/KX/BocNNh93ZJtX5MCQKb/z4yVKgkU2sVIq+xKfg==",
|
||||
"path": "microsoft.data.sqlite/8.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.8.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Data.Sqlite.Core/8.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-pujbzfszX7jAl7oTbHhqx7pxd9jibeyHHl8zy1gd55XMaKWjDtc5XhhNYwQnrwWYCInNdVoArbaaAvLgW7TwuA==",
|
||||
"path": "microsoft.data.sqlite.core/8.0.0",
|
||||
"hashPath": "microsoft.data.sqlite.core.8.0.0.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Web.WebView2/1.0.2903.40": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-THrzYAnJgE3+cNH+9Epr44XjoZoRELdVpXlWGPs6K9C9G6TqyDfVCeVAR/Er8ljLitIUX5gaSkPsy9wRhD1sgQ==",
|
||||
"path": "microsoft.web.webview2/1.0.2903.40",
|
||||
"hashPath": "microsoft.web.webview2.1.0.2903.40.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
|
||||
"path": "sqlitepclraw.bundle_e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.bundle_e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.core/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
|
||||
"path": "sqlitepclraw.core/2.1.6",
|
||||
"hashPath": "sqlitepclraw.core.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.lib.e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==",
|
||||
"path": "sqlitepclraw.lib.e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.lib.e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.provider.e_sqlite3/2.1.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
|
||||
"path": "sqlitepclraw.provider.e_sqlite3/2.1.6",
|
||||
"hashPath": "sqlitepclraw.provider.e_sqlite3.2.1.6.nupkg.sha512"
|
||||
},
|
||||
"System.Diagnostics.EventLog/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-n1ZP7NM2Gkn/MgD8+eOT5MulMj6wfeQMNS2Pizvq5GHCZfjlFMXV2irQlQmJhwA2VABC57M0auudO89Iu2uRLg==",
|
||||
"path": "system.diagnostics.eventlog/8.0.1",
|
||||
"hashPath": "system.diagnostics.eventlog.8.0.1.nupkg.sha512"
|
||||
},
|
||||
"System.IO.Packaging/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-KYkIOAvPexQOLDxPO2g0BVoWInnQhPpkFzRqvNrNrMhVT6kqhVr0zEb6KCHlptLFukxnZrjuMVAnxK7pOGUYrw==",
|
||||
"path": "system.io.packaging/8.0.1",
|
||||
"hashPath": "system.io.packaging.8.0.1.nupkg.sha512"
|
||||
},
|
||||
"System.ServiceProcess.ServiceController/8.0.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-02I0BXo1kmMBgw03E8Hu4K6nTqur4wpQdcDZrndczPzY2fEoGvlinE35AWbyzLZ2h2IksEZ6an4tVt3hi9j1oA==",
|
||||
"path": "system.serviceprocess.servicecontroller/8.0.1",
|
||||
"hashPath": "system.serviceprocess.servicecontroller.8.0.1.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-mddnoBg+XV5YZJg+lp/LlXQ9NY9/oV/MoNjLbbLHw0uTymfyuinVePQB4ff/ELRv3s6n0G7h8q3Ycb3KYg+hgQ==",
|
||||
"path": "uglytoad.pdfpig/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Core/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-bChQUAYApM6/vgBis0+fBTZyAVqjXdqshjZDCgI3dgwUplfLJxXRrnkCOdNj0a6JNcF32R4aLpnGpTc9QmmVmg==",
|
||||
"path": "uglytoad.pdfpig.core/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.core.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Fonts/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-Z6SBBAIL8wRkJNhXGYaz0CrHnNrNeuNtmwRbBtQUA1b3TDhRQppOmHCIuhjb6Vu/Rirp6FIOtzAU1lXsGik90w==",
|
||||
"path": "uglytoad.pdfpig.fonts/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.fonts.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Tokenization/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-U8VVH7VJjv6czP7qWyzDq6CRaiJQe7/sESUCL8H3kiEa3zi0l9TonIKlD/YidQ5DlgTumracii6zjLyKPEFKwA==",
|
||||
"path": "uglytoad.pdfpig.tokenization/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.tokenization.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"UglyToad.PdfPig.Tokens/1.7.0-custom-5": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-m/j5RVfL4eF/OwX6ASprzK+yzD3l7xdgQ7zQPgENhjxfuXD+hj6FSeZlmxSTt9ywvWcTCjGKAILl9XTK9iQgCQ==",
|
||||
"path": "uglytoad.pdfpig.tokens/1.7.0-custom-5",
|
||||
"hashPath": "uglytoad.pdfpig.tokens.1.7.0-custom-5.nupkg.sha512"
|
||||
},
|
||||
"AxCopilot.SDK/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Web.WebView2.Core/1.0.2903.40": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Web.WebView2.WinForms/1.0.2903.40": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Microsoft.Web.WebView2.Wpf/1.0.2903.40": {
|
||||
"type": "reference",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.dll
Normal file
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.exe
Normal file
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/AxCopilot.pdb
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/AxCopilot.Tests/bin/Debug/net8.0-windows/Markdig.dll
Normal file
@@ -0,0 +1,504 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>Microsoft.Web.WebView2.WinForms</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties">
|
||||
<summary>
|
||||
This class is a bundle of the most common parameters used to create <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> and <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/> instances.
|
||||
Its main purpose is to be set to <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> in order to customize the environment and/or controller used by a <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/> during implicit initialization.
|
||||
</summary>
|
||||
<remarks>
|
||||
This class isn't intended to contain all possible environment or controller customization options.
|
||||
If you need complete control over the environment and/or controller used by a WebView2 control then you'll need to initialize the control explicitly by
|
||||
creating your own environment (with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/>) and/or controller options (with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateCoreWebView2ControllerOptions"/>) and passing them to <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/>
|
||||
*before* you set the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property to anything.
|
||||
See the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/> class documentation for an initialization overview.
|
||||
</remarks>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.#ctor">
|
||||
<summary>
|
||||
Creates a new instance of <see cref="T:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties"/> with default data for all properties.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.BrowserExecutableFolder">
|
||||
<summary>
|
||||
Gets or sets the value to pass as the browserExecutableFolder parameter of <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.UserDataFolder">
|
||||
<summary>
|
||||
Gets or sets the value to pass as the userDataFolder parameter of <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.Language">
|
||||
<summary>
|
||||
Gets or sets the value to use for the Language property of the CoreWebView2EnvironmentOptions parameter passed to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.ProfileName">
|
||||
<summary>
|
||||
Gets or sets the value to use for the ProfileName property of the CoreWebView2ControllerOptions parameter passed to CreateCoreWebView2ControllerWithOptionsAsync when creating an controller with this instance.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.AdditionalBrowserArguments">
|
||||
<summary>
|
||||
Gets or sets the value to pass as the AdditionalBrowserArguments parameter of <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions"/> which is passed to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> when creating an environment with this instance.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.IsInPrivateModeEnabled">
|
||||
<summary>
|
||||
Gets or sets the value to use for the IsInPrivateModeEnabled property of the CoreWebView2ControllerOptions parameter passed to CreateCoreWebView2ControllerWithOptionsAsync when creating an controller with this instance.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.CreateEnvironmentAsync">
|
||||
<summary>
|
||||
Create a <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> using the current values of this instance's properties.
|
||||
</summary>
|
||||
<returns>A task which will provide the created environment on completion, or null if no environment-related options are set.</returns>
|
||||
<remarks>
|
||||
As long as no other properties on this instance are changed, repeated calls to this method will return the same task/environment as earlier calls.
|
||||
If some other property is changed then the next call to this method will return a different task/environment.
|
||||
</remarks>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.CoreWebView2CreationProperties.CreateCoreWebView2ControllerOptions(Microsoft.Web.WebView2.Core.CoreWebView2Environment)">
|
||||
<summary>
|
||||
Creates a <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> using the current values of this instance's properties.
|
||||
</summary>
|
||||
<returns>A <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> object or null if no controller-related properties are set.</returns>
|
||||
<exception cref="T:System.NullReferenceException">Thrown if the parameter environment is null.</exception>
|
||||
</member>
|
||||
<member name="T:Microsoft.Web.WebView2.WinForms.WebView2">
|
||||
<summary>
|
||||
Control to embed WebView2 in WinForms.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.#ctor">
|
||||
<summary>
|
||||
Create a new WebView2 WinForms control.
|
||||
After construction the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property is <c>null</c>.
|
||||
Call <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> to initialize the underlying <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/>.
|
||||
</summary>
|
||||
<remarks>
|
||||
This control is effectively a wrapper around the WebView2 COM API, which you can find documentation for here: https://aka.ms/webview2
|
||||
You can directly access the underlying ICoreWebView2 interface and all of its functionality by accessing the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property.
|
||||
Some of the most common COM functionality is also accessible directly through wrapper methods/properties/events on the control.
|
||||
|
||||
Upon creation, the control's CoreWebView2 property will be null.
|
||||
This is because creating the CoreWebView2 is an expensive operation which involves things like launching Edge browser processes.
|
||||
There are two ways to cause the CoreWebView2 to be created:
|
||||
1) Call the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> method. This is referred to as explicit initialization.
|
||||
2) Set the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property. This is referred to as implicit initialization.
|
||||
Either option will start initialization in the background and return back to the caller without waiting for it to finish.
|
||||
To specify options regarding the initialization process, either pass your own <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> to EnsureCoreWebView2Async or set the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> property prior to initialization.
|
||||
|
||||
When initialization has finished (regardless of how it was triggered) then the following things will occur, in this order:
|
||||
1) The control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked. If you need to perform one time setup operations on the CoreWebView2 prior to its use then you should do so in a handler for that event.
|
||||
2) If a Uri has been set to the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property then the control will start navigating to it in the background (i.e. these steps will continue without waiting for the navigation to finish).
|
||||
3) The Task returned from <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> will complete.
|
||||
|
||||
For more details about any of the methods/properties/events involved in the initialization process, see its specific documentation.
|
||||
|
||||
Accelerator key presses (e.g. Ctrl+P) that occur within the control will
|
||||
fire standard key press events such as OnKeyDown. You can suppress the
|
||||
control's default implementation of an accelerator key press (e.g.
|
||||
printing, in the case of Ctrl+P) by setting the Handled property of its
|
||||
EventArgs to true. Also note that the underlying browser process is
|
||||
blocked while these handlers execute, so:
|
||||
<list type="number">
|
||||
<item>
|
||||
You should avoid doing a lot of work in these handlers.
|
||||
</item>
|
||||
<item>
|
||||
Some of the WebView2 and CoreWebView2 APIs may throw errors if
|
||||
invoked within these handlers due to being unable to communicate with
|
||||
the browser process.
|
||||
</item>
|
||||
</list>
|
||||
If you need to do a lot of work and/or invoke WebView2 APIs in response to
|
||||
accelerator keys then consider kicking off a background task or queuing
|
||||
the work for later execution on the UI thread.
|
||||
</remarks>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Dispose(System.Boolean)">
|
||||
<summary>
|
||||
Cleans up any resources being used.
|
||||
</summary>
|
||||
<param name="disposing"><c>true</c> if managed resources should be disposed; otherwise, <c>false</c>.</param>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnPaint(System.Windows.Forms.PaintEventArgs)">
|
||||
<summary>
|
||||
Overrides the base OnPaint event to have custom actions
|
||||
in designer mode
|
||||
</summary>
|
||||
<param name="e">The graphics devices which is the source</param>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.WndProc(System.Windows.Forms.Message@)">
|
||||
<summary>
|
||||
Overrides the base WndProc events to handle specific window messages.
|
||||
</summary>
|
||||
<param name="m">The Message object containing the HWND window message and parameters</param>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties">
|
||||
<summary>
|
||||
Gets or sets a bag of options which are used during initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||
This property cannot be modified (an exception will be thrown) after initialization of the control's CoreWebView2 has started.
|
||||
</summary>
|
||||
<exception cref="T:System.InvalidOperationException">Thrown if initialization of the control's CoreWebView2 has already started.</exception>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)">
|
||||
<summary>
|
||||
Explicitly trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||
</summary>
|
||||
<param name="environment">
|
||||
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> that should be used to create the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||
Creating your own environment gives you control over several options that affect how the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is initialized.
|
||||
If you pass <c>null</c> (the default value) then a default environment will be created and used automatically.
|
||||
</param>
|
||||
<param name="controllerOptions">
|
||||
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions"/> that should be used to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/>.
|
||||
Creating your own controller options gives you control over several options that affect how the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2"/> is initialized.
|
||||
If you pass a controllerOptions to this method then it will override any settings specified on the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> property.
|
||||
If you pass <c>null</c> (the default value) and no value has been set to <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> then a default controllerOptions will be created and used automatically.
|
||||
</param>
|
||||
<returns>
|
||||
A Task that represents the background initialization process.
|
||||
When the task completes then the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property will be available for use (i.e. non-null).
|
||||
Note that the control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked before the task completes
|
||||
or on exceptions.
|
||||
</returns>
|
||||
<remarks>
|
||||
Unless previous initialization has already failed, calling this method additional times with the same parameter will have no effect (any specified environment is ignored) and return the same Task as the first call.
|
||||
Unless previous initialization has already failed, calling this method after initialization has been implicitly triggered by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property will have no effect if no environment is given
|
||||
and simply return a Task representing that initialization already in progress.
|
||||
Unless previous initialization has already failed, calling this method with a different environment after initialization has begun will result in an <see cref="T:System.ArgumentException"/>. For example, this can happen if you begin initialization
|
||||
by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property and then call this method with a new environment, if you begin initialization with <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> and then call this method with a new
|
||||
environment, or if you begin initialization with one environment and then call this method with no environment specified.
|
||||
When this method is called after previous initialization has failed, it will trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> again.
|
||||
Note that even though this method is asynchronous and returns a Task, it still must be called on the UI thread like most public functionality of most UI controls.
|
||||
<para>
|
||||
The following summarizes the possible error values and a description of why these errors occur.
|
||||
<list type="table">
|
||||
<listheader>
|
||||
<description>Error Value</description>
|
||||
<description>Description</description>
|
||||
</listheader>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)</c></description>
|
||||
<description>*\\Edge\\Application* path used in browserExecutableFolder.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_INVALID_STATE)</c></description>
|
||||
<description>Specified options do not match the options of the WebViews that are currently running in the shared browser process.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_INVALID_WINDOW_HANDLE)</c></description>
|
||||
<description>WebView2 Initialization failed due to an invalid host HWND parentWindow.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_DISK_FULL)</c></description>
|
||||
<description>WebView2 Initialization failed due to reaching the maximum number of installed runtime versions.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_PRODUCT_UNINSTALLED</c></description>
|
||||
<description>If the Webview depends upon an installed WebView2 Runtime version and it is uninstalled.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)</c></description>
|
||||
<description>Could not find Edge installation.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>HRESULT_FROM_WIN32(ERROR_FILE_EXISTS)</c></description>
|
||||
<description>User data folder cannot be created because a file with the same name already exists.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>E_ACCESSDENIED</c></description>
|
||||
<description>Unable to create user data folder, Access Denied.</description>
|
||||
</item>
|
||||
<item>
|
||||
<description><c>E_FAIL</c></description>
|
||||
<description>Edge runtime unable to start.</description>
|
||||
</item>
|
||||
</list>
|
||||
</para>
|
||||
</remarks>
|
||||
<exception cref="T:System.ArgumentException">
|
||||
Thrown if this method is called with a different environment than when it was initialized. See Remarks for more info.
|
||||
</exception>
|
||||
<exception cref="T:System.InvalidOperationException">
|
||||
Thrown if this instance of <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is already disposed, or if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.
|
||||
May also be thrown if the browser process has crashed unexpectedly and left the control in an invalid state. We are considering throwing a different type of exception for this case in the future.
|
||||
</exception>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment)">
|
||||
<summary>
|
||||
Explicitly trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||
</summary>
|
||||
<param name="environment">
|
||||
A pre-created <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Environment"/> that should be used to create the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>.
|
||||
Creating your own environment gives you control over several options that affect how the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is initialized.
|
||||
If you pass <c>null</c> then a default environment will be created and used automatically.
|
||||
</param>
|
||||
<returns>
|
||||
A Task that represents the background initialization process.
|
||||
When the task completes then the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> property will be available for use (i.e. non-null).
|
||||
Note that the control's <see cref="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted"/> event will be invoked before the task completes
|
||||
or on exceptions.
|
||||
</returns>
|
||||
<remarks>
|
||||
Unless previous initialization has already failed, calling this method additional times with the same parameter will have no effect (any specified environment is ignored) and return the same Task as the first call.
|
||||
Unless previous initialization has already failed, calling this method after initialization has been implicitly triggered by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property will have no effect if no environment is given
|
||||
and simply return a Task representing that initialization already in progress.
|
||||
Unless previous initialization has already failed, calling this method with a different environment after initialization has begun will result in an <see cref="T:System.ArgumentException"/>. For example, this can happen if you begin initialization
|
||||
by setting the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property and then call this method with a new environment, if you begin initialization with <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CreationProperties"/> and then call this method with a new
|
||||
environment, or if you begin initialization with one environment and then call this method with no environment specified.
|
||||
When this method is called after previous initialization has failed, it will trigger initialization of the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> again.
|
||||
Note that even though this method is asynchronous and returns a Task, it still must be called on the UI thread like most public functionality of most UI controls.
|
||||
</remarks>
|
||||
<exception cref="T:System.ArgumentException">
|
||||
Thrown if this method is called with a different environment than when it was initialized. See Remarks for more info.
|
||||
</exception>
|
||||
<exception cref="T:System.InvalidOperationException">
|
||||
Thrown if this instance of <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is already disposed, or if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.
|
||||
May also be thrown if the browser process has crashed unexpectedly and left the control in an invalid state. We are considering throwing a different type of exception for this case in the future.
|
||||
</exception>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.InitCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)">
|
||||
<summary>
|
||||
This is the private function which implements the actual background initialization task.
|
||||
Cannot be called if the control is already initialized or has been disposed.
|
||||
</summary>
|
||||
<param name="environment">
|
||||
The environment to use to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/>.
|
||||
If that is null then a default environment is created with <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(System.String,System.String,Microsoft.Web.WebView2.Core.CoreWebView2EnvironmentOptions)"/> and its default parameters.
|
||||
</param>
|
||||
<param name="controllerOptions">
|
||||
The controllerOptions to use to create the <see cref="T:Microsoft.Web.WebView2.Core.CoreWebView2Controller"/>.
|
||||
If that is null then a default controllerOptions is created with its default parameters.
|
||||
</param>
|
||||
<returns>A task representing the background initialization process.</returns>
|
||||
<remarks>All the event handlers added here need to be removed in <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.Dispose(System.Boolean)"/>.</remarks>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CreateParams">
|
||||
<summary>
|
||||
Protected CreateParams property. Used to set custom window styles to the forms HWND.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnVisibleChanged(System.EventArgs)">
|
||||
<summary>
|
||||
Protected VisibilityChanged handler.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnSizeChanged(System.EventArgs)">
|
||||
<summary>
|
||||
Protected SizeChanged handler.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Select(System.Boolean,System.Boolean)">
|
||||
<summary>
|
||||
Protected Select method: override this to capture tab direction when WebView control is activated
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnGotFocus(System.EventArgs)">
|
||||
<summary>
|
||||
Protected OnGotFocus handler.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.OnParentChanged(System.EventArgs)">
|
||||
<summary>
|
||||
Protected OnParentChanged handler.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.IsInitialized">
|
||||
<summary>
|
||||
True if initialization finished successfully and the control is not disposed yet.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GetSitedParentSite(System.Windows.Forms.Control)">
|
||||
<summary>
|
||||
Recursive retrieval of the parent control
|
||||
</summary>
|
||||
<param name="control">The control to get the parent for</param>
|
||||
<returns>The root parent control</returns>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2">
|
||||
<summary>
|
||||
The underlying CoreWebView2. Use this property to perform more operations on the WebView2 content than is exposed
|
||||
on the WebView2. This value is null until it is initialized and the object itself has undefined behaviour once the control is disposed.
|
||||
You can force the underlying CoreWebView2 to
|
||||
initialize via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.EnsureCoreWebView2Async(Microsoft.Web.WebView2.Core.CoreWebView2Environment,Microsoft.Web.WebView2.Core.CoreWebView2ControllerOptions)"/> method.
|
||||
</summary>
|
||||
<exception cref="T:System.InvalidOperationException">Thrown if the calling thread isn't the thread which created this object (usually the UI thread). See <see cref="P:System.Windows.Forms.Control.InvokeRequired"/> for more info.</exception>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactor">
|
||||
<summary>
|
||||
The zoom factor for the WebView.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.AllowExternalDrop">
|
||||
<summary>
|
||||
Enable/disable external drop.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.Source">
|
||||
<summary>
|
||||
The Source property is the URI of the top level document of the
|
||||
WebView2. Setting the Source is equivalent to calling <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Navigate(System.String)"/>.
|
||||
Setting the Source will trigger initialization of the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/>, if not already initialized.
|
||||
The default value of Source is <c>null</c>, indicating that the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.
|
||||
</summary>
|
||||
<exception cref="T:System.ArgumentException">Specified value is not an absolute <see cref="T:System.Uri"/>.</exception>
|
||||
<exception cref="T:System.NotImplementedException">Specified value is <c>null</c> and the control is initialized.</exception>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Navigate(System.String)"/>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CanGoForward">
|
||||
<summary>
|
||||
Returns true if the webview can navigate to a next page in the
|
||||
navigation history via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.GoForward"/> method.
|
||||
This is equivalent to the <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoForward"/>.
|
||||
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this property is <c>false</c>.
|
||||
</summary>
|
||||
<seealso cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoForward"/>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.CanGoBack">
|
||||
<summary>
|
||||
Returns <c>true</c> if the webview can navigate to a previous page in the
|
||||
navigation history via the <see cref="M:Microsoft.Web.WebView2.WinForms.WebView2.GoBack"/> method.
|
||||
This is equivalent to the <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoBack"/>.
|
||||
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this property is <c>false</c>.
|
||||
</summary>
|
||||
<seealso cref="P:Microsoft.Web.WebView2.Core.CoreWebView2.CanGoBack"/>
|
||||
</member>
|
||||
<member name="P:Microsoft.Web.WebView2.WinForms.WebView2.DefaultBackgroundColor">
|
||||
<summary>
|
||||
The default background color for the WebView.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.ExecuteScriptAsync(System.String)">
|
||||
<summary>
|
||||
Executes the provided script in the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.ExecuteScriptAsync(System.String)"/>.
|
||||
</summary>
|
||||
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
|
||||
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.ExecuteScriptAsync(System.String)"/>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Reload">
|
||||
<summary>
|
||||
Reloads the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Reload"/>.
|
||||
</summary>
|
||||
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
|
||||
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Reload"/>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GoForward">
|
||||
<summary>
|
||||
Navigates to the next page in navigation history.
|
||||
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoForward"/>.
|
||||
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
|
||||
</summary>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoForward"/>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.GoBack">
|
||||
<summary>
|
||||
Navigates to the previous page in navigation history.
|
||||
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoBack"/>.
|
||||
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
|
||||
</summary>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.GoBack"/>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.NavigateToString(System.String)">
|
||||
<summary>
|
||||
Renders the provided HTML as the top level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.NavigateToString(System.String)"/>.
|
||||
</summary>
|
||||
<exception cref="T:System.InvalidOperationException">The underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized.</exception>
|
||||
<exception cref="T:System.InvalidOperationException">Thrown when browser process has unexpectedly and left this control in an invalid state. We are considering throwing a different type of exception for this case in the future.</exception>
|
||||
<remarks>The <c>htmlContent</c> parameter may not be larger than 2 MB (2 * 1024 * 1024 bytes) in total size. The origin of the new page is <c>about:blank</c>.</remarks>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.NavigateToString(System.String)"/>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.Stop">
|
||||
<summary>
|
||||
Stops any in progress navigation in the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||
This is equivalent to <see cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Stop"/>.
|
||||
If the underlying <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> is not yet initialized, this method does nothing.
|
||||
</summary>
|
||||
<seealso cref="M:Microsoft.Web.WebView2.Core.CoreWebView2.Stop"/>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2InitializationCompleted">
|
||||
<summary>
|
||||
This event is triggered either 1) when the control's <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.CoreWebView2"/> has finished being initialized (regardless of how it was triggered or whether it succeeded) but before it is used for anything
|
||||
OR 2) the initialization failed.
|
||||
You should handle this event if you need to perform one time setup operations on the CoreWebView2 which you want to affect all of its usages
|
||||
(e.g. adding event handlers, configuring settings, installing document creation scripts, adding host objects).
|
||||
</summary>
|
||||
<remarks>
|
||||
This sender will be the WebView2 control, whose CoreWebView2 property will now be valid (i.e. non-null) for the first time
|
||||
if <see cref="P:Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs.IsSuccess"/> is true.
|
||||
Unlikely this event can fire second time (after reporting initialization success first)
|
||||
if the initialization is followed by navigation which fails.
|
||||
</remarks>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.NavigationStarting">
|
||||
<summary>
|
||||
NavigationStarting dispatches before a new navigate starts for the top
|
||||
level document of the <see cref="T:Microsoft.Web.WebView2.WinForms.WebView2"/>.
|
||||
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationStarting"/> event.
|
||||
</summary>
|
||||
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationStarting"/>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.NavigationCompleted">
|
||||
<summary>
|
||||
NavigationCompleted dispatches after a navigate of the top level
|
||||
document completes rendering either successfully or not.
|
||||
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationCompleted"/> event.
|
||||
</summary>
|
||||
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.NavigationCompleted"/>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.WebMessageReceived">
|
||||
<summary>
|
||||
WebMessageReceived dispatches after web content sends a message to the
|
||||
app host via <c>chrome.webview.postMessage</c>.
|
||||
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.WebMessageReceived"/> event.
|
||||
</summary>
|
||||
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.WebMessageReceived"/>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.SourceChanged">
|
||||
<summary>
|
||||
SourceChanged dispatches after the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.Source"/> property changes. This may happen
|
||||
during a navigation or if otherwise the script in the page changes the
|
||||
URI of the document.
|
||||
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.SourceChanged"/> event.
|
||||
</summary>
|
||||
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.SourceChanged"/>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.ContentLoading">
|
||||
<summary>
|
||||
ContentLoading dispatches after a navigation begins to a new URI and the
|
||||
content of that URI begins to render.
|
||||
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.ContentLoading"/> event.
|
||||
</summary>
|
||||
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2.ContentLoading"/>
|
||||
</member>
|
||||
<member name="E:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactorChanged">
|
||||
<summary>
|
||||
ZoomFactorChanged dispatches when the <see cref="P:Microsoft.Web.WebView2.WinForms.WebView2.ZoomFactor"/> property changes.
|
||||
This is equivalent to the <see cref="E:Microsoft.Web.WebView2.Core.CoreWebView2Controller.ZoomFactorChanged"/> event.
|
||||
</summary>
|
||||
<seealso cref="E:Microsoft.Web.WebView2.Core.CoreWebView2Controller.ZoomFactorChanged"/>
|
||||
</member>
|
||||
<member name="F:Microsoft.Web.WebView2.WinForms.WebView2.components">
|
||||
<summary>
|
||||
Required designer variable.
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:Microsoft.Web.WebView2.WinForms.WebView2.InitializeComponent">
|
||||
<summary>
|
||||
Required method for Designer support - do not modify
|
||||
the contents of this method with the code editor.
|
||||
</summary>
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||