journal/Journal.SmokeTests/Program.AiSpeechTests.cs

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);
}
}
}