更新客户端渲染,更新了壳
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using YMhut.Box.Core.App;
|
||||
using YMhut.Box.Core.Plugins;
|
||||
using YMhut.Box.Core.Plugins.Runtime;
|
||||
|
||||
namespace YMhut.Box.Tests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PluginRuntimeTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void RuntimeProtocolRoundTripsShellOutput()
|
||||
{
|
||||
var output = new ShellOutputEvent("session-1", ShellOutputStream.Stdout, "hello", DateTimeOffset.Now, 7);
|
||||
var message = new PluginRuntimeMessage(PluginRuntimeProtocol.ShellOutput, "req-1", ShellOutput: output);
|
||||
|
||||
var parsed = PluginRuntimeProtocol.Deserialize(PluginRuntimeProtocol.Serialize(message));
|
||||
|
||||
Assert.IsNotNull(parsed);
|
||||
Assert.AreEqual(PluginRuntimeProtocol.ShellOutput, parsed.Type);
|
||||
Assert.AreEqual("session-1", parsed.ShellOutput?.SessionId);
|
||||
Assert.AreEqual(ShellOutputStream.Stdout, parsed.ShellOutput?.Stream);
|
||||
Assert.AreEqual("hello", parsed.ShellOutput?.Line);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ManifestV2AcceptsShellCommandInsidePluginRoot()
|
||||
{
|
||||
using var workspace = new TempDirectory(Path.Combine(Path.GetTempPath(), "ymhut-runtime-tests", Guid.NewGuid().ToString("N")));
|
||||
var pluginRoot = CreatePlugin(workspace.Path, "shell-demo", """
|
||||
{
|
||||
"id": "shell-demo",
|
||||
"name": "Shell Demo",
|
||||
"version": "1.0.0",
|
||||
"author": "tester",
|
||||
"description": "Shell plugin",
|
||||
"entry": "index.html",
|
||||
"runtime": "Shell",
|
||||
"permissions": ["ShellExecute", "Log"],
|
||||
"surfaces": [
|
||||
{ "kind": "ToolboxTool", "id": "run", "name": "Run", "description": "Run script", "entry": "index.html" }
|
||||
],
|
||||
"resources": ["index.html", "run.ps1"],
|
||||
"commands": [
|
||||
{ "id": "hello", "name": "Hello", "language": "powershell", "entry": "run.ps1", "timeoutMs": 30000 }
|
||||
],
|
||||
"security": {
|
||||
"requiredPermissions": ["ShellExecute", "Log"],
|
||||
"readPaths": ["."],
|
||||
"writePaths": ["."]
|
||||
}
|
||||
}
|
||||
""");
|
||||
File.WriteAllText(Path.Combine(pluginRoot, "index.html"), "<h1>Shell</h1>");
|
||||
File.WriteAllText(Path.Combine(pluginRoot, "run.ps1"), "Write-Output 'hello'");
|
||||
|
||||
var paths = AppPaths.ForCurrentUser(workspace.Path);
|
||||
var registry = new PluginRegistryService(paths, new PluginStateStore(paths));
|
||||
var plugin = (await registry.LoadPluginsAsync()).Single();
|
||||
|
||||
Assert.IsTrue(plugin.IsValid, string.Join("; ", plugin.Errors));
|
||||
Assert.AreEqual(PluginRuntimeKind.Shell, plugin.Manifest.Runtime);
|
||||
Assert.HasCount(1, plugin.Manifest.Commands!);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ManifestV2RejectsCommandPathEscapingPluginRoot()
|
||||
{
|
||||
using var workspace = new TempDirectory(Path.Combine(Path.GetTempPath(), "ymhut-runtime-tests", Guid.NewGuid().ToString("N")));
|
||||
_ = CreatePlugin(workspace.Path, "bad-shell", """
|
||||
{
|
||||
"id": "bad-shell",
|
||||
"name": "Bad Shell",
|
||||
"version": "1.0.0",
|
||||
"author": "tester",
|
||||
"description": "Bad shell plugin",
|
||||
"entry": "index.html",
|
||||
"runtime": "Shell",
|
||||
"permissions": ["ShellExecute"],
|
||||
"surfaces": [
|
||||
{ "kind": "ToolboxTool", "id": "run", "name": "Run", "description": "Run script", "entry": "index.html" }
|
||||
],
|
||||
"resources": ["index.html"],
|
||||
"commands": [
|
||||
{ "id": "escape", "name": "Escape", "language": "powershell", "entry": "..\\escape.ps1" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
var paths = AppPaths.ForCurrentUser(workspace.Path);
|
||||
var registry = new PluginRegistryService(paths, new PluginStateStore(paths));
|
||||
var plugin = (await registry.LoadPluginsAsync()).Single();
|
||||
|
||||
Assert.IsFalse(plugin.IsValid);
|
||||
Assert.IsTrue(plugin.Errors.Any(error => error.Contains("Command entry", StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private static string CreatePlugin(string root, string folder, string manifest)
|
||||
{
|
||||
var pluginRoot = Path.Combine(root, "Plugins", folder);
|
||||
Directory.CreateDirectory(pluginRoot);
|
||||
File.WriteAllText(Path.Combine(pluginRoot, PluginManifest.FileName), manifest);
|
||||
File.WriteAllText(Path.Combine(pluginRoot, "README.md"), "# Test plugin");
|
||||
return pluginRoot;
|
||||
}
|
||||
|
||||
private sealed class TempDirectory : IDisposable
|
||||
{
|
||||
public TempDirectory(string path)
|
||||
{
|
||||
Path = path;
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SqliteConnection.ClearAllPools();
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,26 +708,27 @@ public sealed class ToolExecutorTests
|
||||
public void UpdateNoticeJsonKeepsPlainTextAndAddsMarkdown()
|
||||
{
|
||||
var repoRoot = Directory.GetParent(FindAssetsRoot())!.FullName;
|
||||
var noticePath = Path.Combine(repoRoot, "update-notice", "2.0.7.5.json");
|
||||
var totalPath = Path.Combine(repoRoot, "update-notice", "total.json");
|
||||
|
||||
using var notice = JsonDocument.Parse(File.ReadAllText(noticePath));
|
||||
var noticeRoot = notice.RootElement;
|
||||
Assert.AreEqual("2.0.7.5", noticeRoot.GetProperty("app_version").GetString());
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("message").GetString()));
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes").GetString()));
|
||||
StringAssert.Contains(noticeRoot.GetProperty("message_md").GetString(), "YMhut Box 2.0.7.5");
|
||||
StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "随机放映室");
|
||||
StringAssert.Contains(noticeRoot.GetProperty("release_notes_md").GetString(), "音量控制");
|
||||
|
||||
using var total = JsonDocument.Parse(File.ReadAllText(totalPath));
|
||||
var totalRoot = total.RootElement;
|
||||
Assert.AreEqual("2.0.7.5", totalRoot.GetProperty("latest_version").GetString());
|
||||
var latestVersion = totalRoot.GetProperty("latest_version").GetString();
|
||||
var latestNoticeFile = totalRoot.GetProperty("latest_notice_file").GetString();
|
||||
var noticePath = Path.Combine(repoRoot, "update-notice", latestNoticeFile!);
|
||||
|
||||
using var notice = JsonDocument.Parse(File.ReadAllText(noticePath));
|
||||
var noticeRoot = notice.RootElement;
|
||||
Assert.AreEqual(latestVersion, noticeRoot.GetProperty("app_version").GetString());
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("message").GetString()));
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes").GetString()));
|
||||
StringAssert.Contains(noticeRoot.GetProperty("message_md").GetString(), $"YMhut Box {latestVersion}");
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(noticeRoot.GetProperty("release_notes_md").GetString()));
|
||||
|
||||
var latest = totalRoot.GetProperty("latest");
|
||||
Assert.AreEqual("2.0.7.5", latest.GetProperty("version").GetString());
|
||||
StringAssert.Contains(latest.GetProperty("release_notes_md").GetString(), "随机放映室");
|
||||
Assert.AreEqual("2.0.7.5", totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
|
||||
StringAssert.Contains(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString(), "随机放映室");
|
||||
Assert.AreEqual(latestVersion, latest.GetProperty("version").GetString());
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latest.GetProperty("release_notes").GetString()));
|
||||
Assert.AreEqual(latestVersion, totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString()));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -49,16 +49,42 @@ public sealed class ToolResultExperienceCatalogTests
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PrivacySanitizerOnlyRedactsYmHutAndApiRequestUrls()
|
||||
public void RepresentativeToolsUseSpecializedProfiles()
|
||||
{
|
||||
var catalog = new ToolCatalog(ToolCatalog.DefaultModules());
|
||||
var experiences = new ToolResultExperienceCatalog(catalog);
|
||||
|
||||
Assert.AreEqual("sanguosha_skin", experiences.GetRequired("sanguosha_skin").Profile);
|
||||
Assert.AreEqual("network_dns", experiences.GetRequired("dns_record_lookup").Profile);
|
||||
Assert.AreEqual("ranked_hotboard", experiences.GetRequired("hotboard").Profile);
|
||||
Assert.AreEqual("code_formatter", experiences.GetRequired("json_formatter").Profile);
|
||||
Assert.AreEqual("security_decode", experiences.GetRequired("base64_codec").Profile);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PrivacySanitizerKeepsNormalUrlsAndRedactsOnlySensitiveFragments()
|
||||
{
|
||||
const string input =
|
||||
"public https://example.com/path?q=1; ymhut https://update.ymhut.cn/update-info.json; api_url=https://api.example.net/v1/items";
|
||||
"public https://example.com/path?q=1; ymhut https://update.ymhut.cn/update-info.json; api_url=https://api.example.net/v1/items?token=abc123; api_key=secret123";
|
||||
|
||||
var sanitized = ToolResultPrivacySanitizer.Redact(input, "en-US");
|
||||
|
||||
StringAssert.Contains(sanitized, "https://example.com/path?q=1");
|
||||
StringAssert.Contains(sanitized, "https://update.ymhut.cn/update-info.json");
|
||||
StringAssert.Contains(sanitized, "https://api.example.net/v1/items?token=[redacted]");
|
||||
Assert.IsFalse(sanitized.Contains("abc123", StringComparison.OrdinalIgnoreCase), sanitized);
|
||||
Assert.IsFalse(sanitized.Contains("secret123", StringComparison.OrdinalIgnoreCase), sanitized);
|
||||
StringAssert.Contains(sanitized, "[redacted]");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PrivacySanitizerCanHideExplicitPrivateEndpointContext()
|
||||
{
|
||||
const string input = "api_url=https://update.ymhut.cn/private/config.json";
|
||||
|
||||
var sanitized = ToolResultPrivacySanitizer.Redact(input, "zh-CN", ["update.ymhut.cn"]);
|
||||
|
||||
Assert.IsFalse(sanitized.Contains("update.ymhut.cn", StringComparison.OrdinalIgnoreCase), sanitized);
|
||||
Assert.IsFalse(sanitized.Contains("api.example.net", StringComparison.OrdinalIgnoreCase), sanitized);
|
||||
StringAssert.Contains(sanitized, "[YMhut endpoint hidden]");
|
||||
StringAssert.Contains(sanitized, "[已隐藏私有接口]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ public sealed class ToolWebPayloadSerializationTests
|
||||
|
||||
Assert.IsNotNull(roundTrip);
|
||||
Assert.HasCount(blocks.Length, roundTrip.ResultDocument.Blocks);
|
||||
Assert.AreEqual("ranked_hotboard", roundTrip.Experience.Profile);
|
||||
Assert.IsFalse(roundTrip.PrivacyPolicy.RedactVisibleContent);
|
||||
Assert.AreEqual("show-original", roundTrip.PrivacyPolicy.VisibleMode);
|
||||
CollectionAssert.Contains(roundTrip.ResultDocument.Blocks.Select(block => block.Kind).ToList(), ToolResultBlockKind.RankedList);
|
||||
CollectionAssert.Contains(roundTrip.ResultDocument.Blocks.Select(block => block.Kind).ToList(), ToolResultBlockKind.Media);
|
||||
StringAssert.Contains(json, nameof(ToolResultBlockKind.RankedList));
|
||||
|
||||
Reference in New Issue
Block a user