|
1 | 1 | using System.Net; |
2 | 2 | using System.Text; |
| 3 | +using System.Text.Json; |
3 | 4 | using System.Text.Json.Nodes; |
4 | 5 | using System.Xml.Linq; |
5 | 6 | using Nova.Core; |
@@ -671,6 +672,33 @@ await CheckAsync("Ollama OpenAI-compatible Agent runtime", async () => |
671 | 672 | Expect(handler.ThinkingExtensionWasOmitted, "Ollama received a provider-specific thinking extension."); |
672 | 673 | }); |
673 | 674 |
|
| 675 | +await CheckAsync("Ollama native API Agent runtime", async () => |
| 676 | +{ |
| 677 | + var handler = new FakeOllamaNativeHandler(); |
| 678 | + var runtime = new DeepSeekChatAgentRuntime(new HttpClient(handler), runtimeEvidence); |
| 679 | + var result = await runtime.RunAsync( |
| 680 | + new AgentRunRequest( |
| 681 | + "ollama-native-smoke", |
| 682 | + "Verify the native Ollama chat protocol.", |
| 683 | + @"D:\Agent", |
| 684 | + string.Empty, |
| 685 | + "ollama", |
| 686 | + "openbmb/minicpm5:latest", |
| 687 | + AgentExecutionMode.Build, |
| 688 | + Endpoint: "http://127.0.0.1:11434/api/chat"), |
| 689 | + _ => Task.CompletedTask, |
| 690 | + _ => throw new Exception("Ollama native smoke unexpectedly requested approval."), |
| 691 | + CancellationToken.None); |
| 692 | + |
| 693 | + Expect(result.Provider == "ollama", "Ollama native provider metadata was not retained."); |
| 694 | + Expect(result.FinalText == "Ollama native API connected.", "Ollama NDJSON response was not assembled."); |
| 695 | + Expect(handler.UsedNativeEndpoint, "Ollama native endpoint was not used."); |
| 696 | + Expect(handler.UsedNativeRequestShape, "Ollama native request fields were not normalized."); |
| 697 | + Expect(handler.UsedExpandedContextWindow, "Ollama native request did not reserve an expanded context window."); |
| 698 | + Expect(handler.AcceptedNdjson, "Ollama native request did not advertise NDJSON."); |
| 699 | + Expect(handler.AuthorizationWasOmitted, "Ollama native request received an unnecessary Authorization header."); |
| 700 | +}); |
| 701 | + |
674 | 702 | await CheckAsync("Kimi multimodal API and bounded attachments", async () => |
675 | 703 | { |
676 | 704 | var temporaryDirectory = Path.Combine( |
@@ -5516,6 +5544,30 @@ await CheckAsync("Electron 1.0 trustworthy cross-model delivery contract", async |
5516 | 5544 | "The 1.0 renderer can no longer opt into cross-model review or expose truthful partial delivery."); |
5517 | 5545 | }); |
5518 | 5546 |
|
| 5547 | +await CheckAsync("Electron Ollama native endpoint contract", async () => |
| 5548 | +{ |
| 5549 | + var mainSource = await File.ReadAllTextAsync( |
| 5550 | + @"D:\Agent\NovaDesktop.Electron\electron\main.cjs"); |
| 5551 | + var rendererSource = await File.ReadAllTextAsync( |
| 5552 | + @"D:\Agent\NovaDesktop.Electron\src\App.tsx"); |
| 5553 | + Expect( |
| 5554 | + mainSource.Contains( |
| 5555 | + "endpoint: \"http://localhost:11434/api/chat\"", |
| 5556 | + StringComparison.Ordinal) |
| 5557 | + && mainSource.Contains( |
| 5558 | + "provider === \"ollama\" && /\\/api\\/chat$/i.test(pathname)", |
| 5559 | + StringComparison.Ordinal) |
| 5560 | + && mainSource.Contains( |
| 5561 | + "endpoint.pathname.replace(/\\/api\\/chat$/i, \"/api/tags\")", |
| 5562 | + StringComparison.Ordinal), |
| 5563 | + "Electron no longer preserves Ollama's native /api/chat endpoint and matching model probe path."); |
| 5564 | + Expect( |
| 5565 | + mainSource.Contains("Ollama 服务已连接,但没有发现已安装模型", StringComparison.Ordinal) |
| 5566 | + && mainSource.Contains("Ollama 中未找到模型", StringComparison.Ordinal) |
| 5567 | + && rendererSource.Contains("http://localhost:11434", StringComparison.Ordinal), |
| 5568 | + "Electron no longer gives an actionable missing-model error or localhost guidance."); |
| 5569 | +}); |
| 5570 | + |
5519 | 5571 | await CheckAsync("Electron bridge non-blocking start and lease retry contract", async () => |
5520 | 5572 | { |
5521 | 5573 | var bridgeSource = await File.ReadAllTextAsync( |
@@ -5921,6 +5973,46 @@ protected override async Task<HttpResponseMessage> SendAsync( |
5921 | 5973 | } |
5922 | 5974 | } |
5923 | 5975 |
|
| 5976 | +file sealed class FakeOllamaNativeHandler : HttpMessageHandler |
| 5977 | +{ |
| 5978 | + public bool UsedNativeEndpoint { get; private set; } |
| 5979 | + public bool UsedNativeRequestShape { get; private set; } |
| 5980 | + public bool UsedExpandedContextWindow { get; private set; } |
| 5981 | + public bool AcceptedNdjson { get; private set; } |
| 5982 | + public bool AuthorizationWasOmitted { get; private set; } |
| 5983 | + |
| 5984 | + protected override async Task<HttpResponseMessage> SendAsync( |
| 5985 | + HttpRequestMessage request, |
| 5986 | + CancellationToken cancellationToken) |
| 5987 | + { |
| 5988 | + UsedNativeEndpoint = request.RequestUri?.ToString() |
| 5989 | + .Equals("http://127.0.0.1:11434/api/chat", StringComparison.OrdinalIgnoreCase) == true; |
| 5990 | + AuthorizationWasOmitted = request.Headers.Authorization is null; |
| 5991 | + AcceptedNdjson = request.Headers.Accept.Any(item => |
| 5992 | + item.MediaType?.Equals("application/x-ndjson", StringComparison.OrdinalIgnoreCase) == true); |
| 5993 | + var body = await request.Content!.ReadAsStringAsync(cancellationToken); |
| 5994 | + UsedNativeRequestShape = body.Contains("\"options\"", StringComparison.Ordinal) |
| 5995 | + && body.Contains("\"num_predict\"", StringComparison.Ordinal) |
| 5996 | + && !body.Contains("\"max_tokens\"", StringComparison.Ordinal) |
| 5997 | + && !body.Contains("\"stream_options\"", StringComparison.Ordinal) |
| 5998 | + && !body.Contains("\"tool_choice\"", StringComparison.Ordinal) |
| 5999 | + && !body.Contains("\"thinking\"", StringComparison.Ordinal); |
| 6000 | + using var payload = JsonDocument.Parse(body); |
| 6001 | + UsedExpandedContextWindow = payload.RootElement |
| 6002 | + .GetProperty("options") |
| 6003 | + .GetProperty("num_ctx") |
| 6004 | + .GetInt32() >= 8192; |
| 6005 | + const string response = """ |
| 6006 | + {"model":"openbmb/minicpm5:latest","created_at":"2026-07-31T10:00:00Z","message":{"role":"assistant","content":"Ollama native API connected."},"done":false} |
| 6007 | + {"model":"openbmb/minicpm5:latest","created_at":"2026-07-31T10:00:01Z","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop"} |
| 6008 | + """; |
| 6009 | + return new HttpResponseMessage(HttpStatusCode.OK) |
| 6010 | + { |
| 6011 | + Content = new StringContent(response, Encoding.UTF8, "application/x-ndjson") |
| 6012 | + }; |
| 6013 | + } |
| 6014 | +} |
| 6015 | + |
5924 | 6016 | file sealed class FakeDeepSeekHandler : HttpMessageHandler |
5925 | 6017 | { |
5926 | 6018 | public int RequestCount { get; private set; } |
|
0 commit comments