343 lines
14 KiB
C#
343 lines
14 KiB
C#
internal static partial class Program
|
|
{
|
|
static async Task TestEntryAiHealthDefaultAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var response = await entry.HandleCommandAsync("""{"action":"ai.health"}""");
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for ai.health.");
|
|
var data = doc.RootElement.GetProperty("data");
|
|
Assert(data.GetProperty("Enabled").GetBoolean() is false, "Expected AI disabled by default.");
|
|
Assert(string.Equals(data.GetProperty("Provider").GetString(), "none", StringComparison.OrdinalIgnoreCase), "Expected default provider 'none'.");
|
|
}
|
|
|
|
static async Task TestEntryAiSummarizeEntryDisabledAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "ai.summarize_entry",
|
|
payload = new
|
|
{
|
|
content = "sample entry"
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.summarize_entry.");
|
|
var data = doc.RootElement.GetProperty("data").GetString() ?? "";
|
|
Assert(data.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled provider message for ai.summarize_entry.");
|
|
}
|
|
|
|
static async Task TestEntryAiSummarizeAllDisabledAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "ai.summarize_all",
|
|
payload = new
|
|
{
|
|
entries = new[] { "entry one", "entry two" }
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.summarize_all.");
|
|
var data = doc.RootElement.GetProperty("data").GetString() ?? "";
|
|
Assert(data.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled provider message for ai.summarize_all.");
|
|
}
|
|
|
|
static async Task TestEntryAiChatDisabledAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "ai.chat",
|
|
payload = new
|
|
{
|
|
prompt = "hello cloud"
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.chat.");
|
|
var data = doc.RootElement.GetProperty("data").GetString() ?? "";
|
|
Assert(data.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled provider message for ai.chat.");
|
|
}
|
|
|
|
static async Task TestEntryAiEmbedDisabledAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "ai.embed",
|
|
payload = new
|
|
{
|
|
content = "embedding source text"
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for disabled ai.embed.");
|
|
var data = doc.RootElement.GetProperty("data");
|
|
Assert(data.ValueKind == JsonValueKind.Array, "Expected ai.embed response to be a JSON array.");
|
|
Assert(data.GetArrayLength() == 0, "Expected disabled ai.embed to return an empty vector.");
|
|
}
|
|
|
|
static async Task TestEntrySpeechDevicesListDisabledAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "speech.devices.list",
|
|
payload = new { }
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for speech.devices.list when disabled.");
|
|
var data = doc.RootElement.GetProperty("data");
|
|
Assert(data.ValueKind == JsonValueKind.Object, "Expected speech.devices.list data to be an object.");
|
|
}
|
|
|
|
static async Task TestEntrySpeechTranscribeDisabledAsync()
|
|
{
|
|
var entry = NewEntry();
|
|
var request = JsonSerializer.Serialize(new
|
|
{
|
|
action = "speech.transcribe",
|
|
payload = new
|
|
{
|
|
text = "fixture transcript",
|
|
engine = "whisper"
|
|
}
|
|
});
|
|
|
|
var response = await entry.HandleCommandAsync(request);
|
|
using var doc = JsonDocument.Parse(response);
|
|
Assert(doc.RootElement.GetProperty("ok").GetBoolean(), "Expected ok=true for speech.transcribe when disabled.");
|
|
var data = doc.RootElement.GetProperty("data");
|
|
Assert(data.ValueKind == JsonValueKind.Object, "Expected speech.transcribe data to be an object.");
|
|
var warning = data.TryGetProperty("Warning", out var warningNode) ? warningNode.GetString() ?? "" : "";
|
|
Assert(warning.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled speech warning.");
|
|
}
|
|
|
|
static async Task TestPythonSidecarAiServiceJsonLineAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-ai-smoke", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var scriptPath = Path.Combine(root, "fake_ai_sidecar.py");
|
|
File.WriteAllText(scriptPath, """
|
|
import json, sys
|
|
request = json.loads(sys.stdin.readline())
|
|
action = request.get("action", "")
|
|
print("DEBUG prelude")
|
|
if action == "health":
|
|
print(json.dumps({"ok": True, "data": {"provider": "python-sidecar", "healthy": True, "message": "ok"}}))
|
|
elif action == "summarize_entry":
|
|
payload = request.get("payload") or {}
|
|
print(json.dumps({"ok": True, "data": "ENTRY::" + str(payload.get("content", ""))}))
|
|
elif action == "summarize_all":
|
|
payload = request.get("payload") or {}
|
|
entries = payload.get("entries") or []
|
|
print(json.dumps({"ok": True, "data": "ALL::" + str(len(entries))}))
|
|
elif action == "chat":
|
|
payload = request.get("payload") or {}
|
|
print(json.dumps({"ok": True, "data": "CHAT::" + str(payload.get("prompt", ""))}))
|
|
elif action == "embed":
|
|
payload = request.get("payload") or {}
|
|
text = str(payload.get("content", ""))
|
|
print(json.dumps({"ok": True, "data": [float(len(text)), 2.5, -1.0]}))
|
|
else:
|
|
print(json.dumps({"ok": False, "error": "unknown action"}))
|
|
""");
|
|
|
|
try
|
|
{
|
|
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
|
|
IAiService service = new PythonSidecarAiService(config);
|
|
|
|
var health = await service.HealthAsync();
|
|
Assert(health.Enabled, "Expected enabled=true for python-sidecar health.");
|
|
Assert(health.Healthy, "Expected healthy=true from fake sidecar health.");
|
|
|
|
var one = await service.SummarizeEntryAsync("hello");
|
|
Assert(one == "ENTRY::hello", "Unexpected summarize_entry response.");
|
|
|
|
var all = await service.SummarizeAllAsync(["a", "b", "c"]);
|
|
Assert(all == "ALL::3", "Unexpected summarize_all response.");
|
|
|
|
var chat = await service.ChatAsync("hello");
|
|
Assert(chat == "CHAT::hello", "Unexpected chat response.");
|
|
|
|
var vector = await service.EmbedAsync("hello");
|
|
Assert(vector.Count == 3, "Unexpected embed vector length.");
|
|
Assert(Math.Abs(vector[0] - 5d) < 0.0001d, "Unexpected embed vector first value.");
|
|
Assert(Math.Abs(vector[1] - 2.5d) < 0.0001d, "Unexpected embed vector second value.");
|
|
Assert(Math.Abs(vector[2] + 1.0d) < 0.0001d, "Unexpected embed vector third value.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestPythonSidecarAiServiceErrorAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-ai-smoke", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var scriptPath = Path.Combine(root, "fake_ai_sidecar_error.py");
|
|
File.WriteAllText(scriptPath, """
|
|
import json, sys
|
|
_ = json.loads(sys.stdin.readline())
|
|
print(json.dumps({"ok": False, "error": "simulated failure"}))
|
|
""");
|
|
|
|
try
|
|
{
|
|
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
|
|
IAiService service = new PythonSidecarAiService(config);
|
|
|
|
try
|
|
{
|
|
_ = await service.SummarizeEntryAsync("hello");
|
|
}
|
|
catch (InvalidOperationException ex) when (ex.Message.Contains("simulated failure", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException("Expected summarize_entry to surface sidecar error.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestPythonSidecarSpeechServiceNoDevicesAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var scriptPath = Path.Combine(root, "fake_speech_sidecar_nodes.py");
|
|
File.WriteAllText(scriptPath, """
|
|
import json, sys
|
|
request = json.loads(sys.stdin.readline())
|
|
action = request.get("action", "")
|
|
if action == "speech.devices.list":
|
|
print(json.dumps({"ok": True, "data": {"devices": [], "warning": "no devices"}}))
|
|
elif action == "speech.transcribe":
|
|
payload = request.get("payload") or {}
|
|
print(json.dumps({"ok": True, "data": {"text": payload.get("text", ""), "engine": payload.get("engine", "whisper")}}))
|
|
else:
|
|
print(json.dumps({"ok": False, "error": "unknown action"}))
|
|
""");
|
|
|
|
try
|
|
{
|
|
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
|
|
ISpeechBridgeService service = new PythonSidecarSpeechService(config);
|
|
|
|
var devices = await service.ListDevicesAsync();
|
|
Assert(devices.Devices.Count == 0, "Expected empty devices list.");
|
|
Assert((devices.Warning ?? "").Contains("no devices", StringComparison.OrdinalIgnoreCase), "Expected no-devices warning.");
|
|
|
|
var transcript = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture text", Engine: "whisper"));
|
|
Assert(transcript.Text == "fixture text", "Expected passthrough transcript text.");
|
|
Assert(transcript.Engine == "whisper", "Expected passthrough transcript engine.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestPythonSidecarSpeechServiceErrorAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var scriptPath = Path.Combine(root, "fake_speech_sidecar_error.py");
|
|
File.WriteAllText(scriptPath, """
|
|
import json, sys
|
|
request = json.loads(sys.stdin.readline())
|
|
action = request.get("action", "")
|
|
if action == "speech.transcribe":
|
|
print(json.dumps({"ok": False, "error": "engine unavailable"}))
|
|
else:
|
|
print(json.dumps({"ok": True, "data": {"devices": []}}))
|
|
""");
|
|
|
|
try
|
|
{
|
|
var config = BuildAiConfig(scriptPath, timeoutMs: 4000);
|
|
ISpeechBridgeService service = new PythonSidecarSpeechService(config);
|
|
|
|
try
|
|
{
|
|
_ = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture", Engine: "faster-whisper"));
|
|
}
|
|
catch (InvalidOperationException ex) when (ex.Message.Contains("engine unavailable", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException("Expected speech transcribe to surface sidecar engine error.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
static async Task TestPythonSidecarSpeechServiceTimeoutAsync()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var scriptPath = Path.Combine(root, "fake_speech_sidecar_timeout.py");
|
|
File.WriteAllText(scriptPath, """
|
|
import json, sys, time
|
|
request = json.loads(sys.stdin.readline())
|
|
payload = request.get("payload") or {}
|
|
sleep_ms = int(payload.get("simulate_delay_ms") or 0)
|
|
time.sleep(max(0, sleep_ms) / 1000.0)
|
|
print(json.dumps({"ok": True, "data": {"text": "", "engine": "whisper"}}))
|
|
""");
|
|
|
|
try
|
|
{
|
|
var config = BuildAiConfig(scriptPath, timeoutMs: 100);
|
|
ISpeechBridgeService service = new PythonSidecarSpeechService(config);
|
|
|
|
try
|
|
{
|
|
_ = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture", SimulateDelayMs: 500));
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException("Expected speech transcribe timeout path.");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|