| 1 | using System.Text.Json; |
| 2 | using System.Text.RegularExpressions; |
| 3 | using CascadeIDE.Features.WebAiPortal.Application; |
| 4 | using Xunit; |
| 5 | |
| 6 | namespace CascadeIDE.Tests; |
| 7 | |
| 8 | /// <summary>Контракт артефактов ADR 0108 в <c>AiPrompts/</c>: .md (json-cascade) и .chat-paste.txt (голый однострочный JSON) против whitelist моста.</summary> |
| 9 | public sealed class WebAiPortalAdr0108PromptVerificationTests |
| 10 | { |
| 11 | private static readonly Regex JsonCascadeFence = new( |
| 12 | """```json-cascade[ \t]*\r?\n(.*?)```""", |
| 13 | RegexOptions.Singleline | RegexOptions.CultureInvariant, |
| 14 | TimeSpan.FromSeconds(1)); |
| 15 | |
| 16 | private static string PromptPath => |
| 17 | Path.Combine(AppContext.BaseDirectory, "AiPrompts", "web-ai-portal-bridge-adr0108.prompts.md"); |
| 18 | |
| 19 | private static string ChatPastePath => |
| 20 | Path.Combine(AppContext.BaseDirectory, "AiPrompts", "web-ai-portal-bridge-adr0108.chat-paste.txt"); |
| 21 | |
| 22 | [Fact] |
| 23 | public void Prompt_file_shipped_next_to_tests() |
| 24 | { |
| 25 | Assert.True(File.Exists(PromptPath), "ожидается CopyToOutputDirectory в CascadeIDE.Tests.csproj: " + PromptPath); |
| 26 | } |
| 27 | |
| 28 | [Fact] |
| 29 | public void Chat_paste_plain_file_shipped_next_to_tests() |
| 30 | { |
| 31 | Assert.True(File.Exists(ChatPastePath), "ожидается CopyToOutputDirectory в CascadeIDE.Tests.csproj: " + ChatPastePath); |
| 32 | } |
| 33 | |
| 34 | /// <summary>Текст без Markdown: каждая однострочная JSON-команда должна распознаваться мостом как голый буфер и входить в whitelist.</summary> |
| 35 | [Fact] |
| 36 | public void Chat_paste_plain_examples_are_allowlisted_when_parsed_like_clipboard_payload() |
| 37 | { |
| 38 | var txt = File.ReadAllText(ChatPastePath); |
| 39 | Assert.Contains("0108-web-ai-portal-host-object-tools-bridge.md", txt, StringComparison.Ordinal); |
| 40 | |
| 41 | var exampleLines = 0; |
| 42 | foreach (var segment in txt.Split('\n')) |
| 43 | { |
| 44 | var line = segment.Trim('\r').Trim(); |
| 45 | if (line.Length < 18 || line[0] != '{') |
| 46 | continue; |
| 47 | if (!WebAiPortalBridgePayloadResolution.TryParseBareExecuteCommand(line, out var json)) |
| 48 | continue; |
| 49 | Assert.True(WebAiPortalBridgePayloadResolution.TryGetCommandId(json!, out var id)); |
| 50 | Assert.True( |
| 51 | WebAiPortalCommandBridge.Whitelist.Contains(id!), |
| 52 | $"строка-пример с command_id «{id}» вне whitelist"); |
| 53 | exampleLines++; |
| 54 | } |
| 55 | |
| 56 | Assert.True(exampleLines >= 5, $"ожидалось ≥5 однострочных примеров JSON, получено {exampleLines}"); |
| 57 | } |
| 58 | |
| 59 | [Fact] |
| 60 | public void Prompt_md_points_reader_to_chat_paste_plain_sibling_for_models_that_strip_markdown() |
| 61 | { |
| 62 | Assert.Contains( |
| 63 | "web-ai-portal-bridge-adr0108.chat-paste.txt", |
| 64 | File.ReadAllText(PromptPath), |
| 65 | StringComparison.Ordinal); |
| 66 | } |
| 67 | |
| 68 | [Fact] |
| 69 | public void Prompt_references_canonical_ADR_doc_path_and_each_fence_is_allowlisted_execute_command() |
| 70 | { |
| 71 | var md = File.ReadAllText(PromptPath); |
| 72 | Assert.Contains("0108-web-ai-portal-host-object-tools-bridge.md", md, StringComparison.Ordinal); |
| 73 | Assert.Contains("ADR 0108", md, StringComparison.Ordinal); |
| 74 | |
| 75 | var matches = JsonCascadeFence.Matches(md); |
| 76 | Assert.True(matches.Count >= 2, "промпт должен содержать хотя бы два fenced json-cascade"); |
| 77 | |
| 78 | foreach (Match m in matches) |
| 79 | { |
| 80 | Assert.True(m.Success && m.Groups.Count >= 2); |
| 81 | var inner = m.Groups[1].Value.Trim(); |
| 82 | Assert.False(string.IsNullOrWhiteSpace(inner)); |
| 83 | |
| 84 | using var doc = JsonDocument.Parse(inner); |
| 85 | Assert.Equal(JsonValueKind.Object, doc.RootElement.ValueKind); |
| 86 | Assert.True(doc.RootElement.TryGetProperty("command_id", out var cid)); |
| 87 | Assert.Equal(JsonValueKind.String, cid.ValueKind); |
| 88 | var id = cid.GetString(); |
| 89 | Assert.False(string.IsNullOrWhiteSpace(id)); |
| 90 | Assert.True( |
| 91 | WebAiPortalCommandBridge.Whitelist.Contains(id!), |
| 92 | $"command_id «{id}» не из whitelist моста ADR 0108"); |
| 93 | Assert.False( |
| 94 | string.Equals(IdeCommands.CodebaseIndexReindex, id, StringComparison.Ordinal), |
| 95 | "промпт ADR0108 только read-PoC: reindex не в whitelist моста"); |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |