mirror of
https://kevinblog.sytes.net/Code/Jibo-Revival-Group/JiboExperiments.git
synced 2026-06-15 09:36:34 +00:00
Document local cloud startup and harden persistence
This commit is contained in:
@@ -14,6 +14,16 @@ We are rebuilding the hosted cloud first, then using that foundation for OTA, Op
|
||||
|
||||
Current release truth lives in [docs/development-plan.md](docs/development-plan.md). The current cloud release constant is `1.0.19`.
|
||||
|
||||
## Running Locally
|
||||
|
||||
For local setup, use [docs/local-cloud-quickstart.md](docs/local-cloud-quickstart.md).
|
||||
|
||||
It covers:
|
||||
|
||||
- the current `.NET` OpenJibo cloud
|
||||
- the legacy Node protocol oracle
|
||||
- the Playground direct-to-Jibo ASR/TTS demo
|
||||
|
||||
## Roadmap
|
||||
|
||||
The long-range plan is summarized in [docs/roadmap.md](docs/roadmap.md). In short:
|
||||
@@ -94,6 +104,7 @@ OpenJibo/
|
||||
Use these when you want the active technical truth:
|
||||
|
||||
- [Development plan](docs/development-plan.md)
|
||||
- [Local cloud quickstart](docs/local-cloud-quickstart.md)
|
||||
- [Feature backlog](docs/feature-backlog.md)
|
||||
- [Release 1.0.19 plan](docs/release-1.0.19-plan.md)
|
||||
- [Support tiers](docs/support-tiers.md)
|
||||
|
||||
153
OpenJibo/docs/local-cloud-quickstart.md
Normal file
153
OpenJibo/docs/local-cloud-quickstart.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Local OpenJibo Cloud Quickstart
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide is for people who want to run OpenJibo locally.
|
||||
|
||||
There are three different local paths:
|
||||
|
||||
- `.NET cloud`: the current OpenJibo cloud implementation and the path we are actively shipping.
|
||||
- `Node cloud`: the legacy protocol oracle and reverse-engineering server. It is still useful and fun to run, but it is not the production direction.
|
||||
- `Playground`: a direct local Jibo ASR/TTS demo. It talks to Jibo on local ports and does not replace the cloud.
|
||||
|
||||
For a physical Jibo, local cloud testing still assumes a controlled network, DNS/host routing, and certificate setup. See [device-bootstrap.md](device-bootstrap.md) for the device side.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install:
|
||||
|
||||
- .NET SDK for the repo target framework
|
||||
- Node.js and npm, for the Node oracle only
|
||||
- PowerShell, for the Windows helper scripts
|
||||
- `openssl`, for Linux live testing on port `443` with PEM certificate material
|
||||
|
||||
Optional for real audio experiments:
|
||||
|
||||
- `ffmpeg`
|
||||
- `whisper.cpp`
|
||||
|
||||
## Run The .NET Cloud
|
||||
|
||||
From the repo root:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboDotNet.ps1
|
||||
```
|
||||
|
||||
By default, this starts:
|
||||
|
||||
- HTTPS: `https://localhost:24604`
|
||||
- HTTP: `http://localhost:24605`
|
||||
- health check: `http://localhost:24605/health`
|
||||
- websocket captures: `captures/websocket`
|
||||
- HTTP captures: `captures/http`
|
||||
|
||||
Smoke check:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Invoke-CloudSmoke.ps1 -BaseUrl http://localhost:24605
|
||||
```
|
||||
|
||||
Run with the Azure Blob sample launch profile:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboDotNet.ps1 -UseAzureBlobProfile
|
||||
```
|
||||
|
||||
Run directly without a launch profile, useful when you want to supply all URLs and certificate settings by environment:
|
||||
|
||||
```powershell
|
||||
$env:ASPNETCORE_URLS = "http://0.0.0.0:24605"
|
||||
.\scripts\cloud\Start-OpenJiboDotNet.ps1 -NoLaunchProfile
|
||||
```
|
||||
|
||||
For a Linux live-device run on port `443`, reuse the existing PEM certificate material:
|
||||
|
||||
```bash
|
||||
CERT_PEM=/path/to/cert.pem \
|
||||
KEY_PEM=/path/to/key.pem \
|
||||
ASPNETCORE_URLS="https://0.0.0.0:443;http://0.0.0.0:24605" \
|
||||
./scripts/cloud/start-dotnet-with-node-cert.sh
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
./scripts/cloud/invoke-live-jibo-prep.sh
|
||||
```
|
||||
|
||||
## Run The Node Cloud
|
||||
|
||||
The Node cloud lives at `src/Jibo.Cloud/node`.
|
||||
|
||||
From the repo root:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboNode.ps1 -Install
|
||||
```
|
||||
|
||||
After dependencies are installed once, you can usually run:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboNode.ps1
|
||||
```
|
||||
|
||||
Important details:
|
||||
|
||||
- The Node server binds HTTPS on port `443`.
|
||||
- It expects `cert.pem` and `key.pem` in `src/Jibo.Cloud/node`.
|
||||
- Use the same certificate material that your controlled Jibo routing already trusts.
|
||||
- On Windows or Linux, binding port `443` may require an elevated shell.
|
||||
- Stop the .NET cloud first if it is also using port `443`.
|
||||
|
||||
Manual equivalent:
|
||||
|
||||
```powershell
|
||||
cd src\Jibo.Cloud\node
|
||||
npm install
|
||||
node .\open-jibo-link.js
|
||||
```
|
||||
|
||||
The Node server writes discovery logs under `src/Jibo.Cloud/node/logs`.
|
||||
|
||||
## Run Playground
|
||||
|
||||
Playground is not a cloud server. It connects straight to a Jibo on your LAN:
|
||||
|
||||
- ASR HTTP: `http://JIBO_IP:8088/asr_simple_interface`
|
||||
- ASR websocket: `ws://JIBO_IP:8088/simple_port`
|
||||
- TTS HTTP: `http://JIBO_IP:8089/tts_speak`
|
||||
|
||||
From the repo root:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboPlayground.ps1
|
||||
```
|
||||
|
||||
When prompted, enter the Jibo IP address.
|
||||
|
||||
Use Playground when you want to test the local ASR/TTS client behavior directly. Use the `.NET` or Node cloud when you want Jibo to boot and talk through the cloud-shaped protocol path.
|
||||
|
||||
## Which One Should I Use?
|
||||
|
||||
Use `.NET cloud` if you want the current OpenJibo behavior, release testing, captures, or anything close to the hosted future.
|
||||
|
||||
Use `Node cloud` if you want the original prototype/oracle, protocol discovery, or a quick comparison against older behavior.
|
||||
|
||||
Use `Playground` if you already know the robot IP and just want a local microphone-to-ASR-to-TTS loop through Jibo's local client interfaces.
|
||||
|
||||
## Common Issues
|
||||
|
||||
If `/health` fails, confirm the .NET cloud is running and use `http://localhost:24605/health` for local checks.
|
||||
|
||||
If the Node server fails with a certificate error, add `cert.pem` and `key.pem` to `src/Jibo.Cloud/node`.
|
||||
|
||||
If port `443` is busy, stop the other cloud server first or run the .NET cloud on the local dev ports.
|
||||
|
||||
If a physical Jibo does not connect, confirm DNS/host routing for:
|
||||
|
||||
- `api.jibo.com`
|
||||
- `api-socket.jibo.com`
|
||||
- `neo-hub.jibo.com`
|
||||
|
||||
Then compare with the live runbook in [live-jibo-test-runbook.md](live-jibo-test-runbook.md).
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
These scripts help exercise the new .NET hosted cloud locally.
|
||||
|
||||
- `Start-OpenJiboDotNet.ps1`
|
||||
Starts the current `.NET` cloud with local capture directories configured.
|
||||
- `Start-OpenJiboNode.ps1`
|
||||
Starts the legacy Node protocol oracle from `src/Jibo.Cloud/node`.
|
||||
- `Start-OpenJiboPlayground.ps1`
|
||||
Starts the direct local Jibo ASR/TTS Playground demo.
|
||||
- `Invoke-CloudSmoke.ps1`
|
||||
Runs a few quick HTTP checks against a local OpenJibo cloud instance.
|
||||
- `Invoke-ProtocolFixture.ps1`
|
||||
@@ -26,3 +32,5 @@ These scripts help exercise the new .NET hosted cloud locally.
|
||||
Bash summary helper for captured websocket telemetry and exported fixtures.
|
||||
- `import-websocket-capture-fixture.py`
|
||||
Cross-platform import/sanitization helper for exported websocket fixtures.
|
||||
|
||||
See [docs/local-cloud-quickstart.md](../../docs/local-cloud-quickstart.md) for the full local setup guide.
|
||||
|
||||
41
OpenJibo/scripts/cloud/Start-OpenJiboDotNet.ps1
Normal file
41
OpenJibo/scripts/cloud/Start-OpenJiboDotNet.ps1
Normal file
@@ -0,0 +1,41 @@
|
||||
param(
|
||||
[string]$ProjectPath = "src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Api/Jibo.Cloud.Api.csproj",
|
||||
[string]$LaunchProfile = "Jibo.Cloud.Api",
|
||||
[string]$CaptureRoot = "captures",
|
||||
[switch]$UseAzureBlobProfile,
|
||||
[switch]$NoLaunchProfile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\.."))
|
||||
$resolvedProjectPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $ProjectPath))
|
||||
$resolvedCaptureRoot = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $CaptureRoot))
|
||||
$webSocketCaptureDirectory = Join-Path $resolvedCaptureRoot "websocket"
|
||||
$httpCaptureDirectory = Join-Path $resolvedCaptureRoot "http"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $resolvedProjectPath)) {
|
||||
throw "Could not find .NET API project at $resolvedProjectPath"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $webSocketCaptureDirectory | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $httpCaptureDirectory | Out-Null
|
||||
|
||||
$env:OpenJibo__Telemetry__DirectoryPath = $webSocketCaptureDirectory
|
||||
$env:OpenJibo__ProtocolTelemetry__DirectoryPath = $httpCaptureDirectory
|
||||
|
||||
if ($UseAzureBlobProfile) {
|
||||
$LaunchProfile = "Jibo.Cloud.Api.AzureBlob"
|
||||
}
|
||||
|
||||
Write-Host "Starting OpenJibo .NET cloud"
|
||||
Write-Host " - project: $resolvedProjectPath"
|
||||
Write-Host " - websocket captures: $webSocketCaptureDirectory"
|
||||
Write-Host " - http captures: $httpCaptureDirectory"
|
||||
if ($NoLaunchProfile) {
|
||||
Write-Host " - launch profile: disabled"
|
||||
dotnet run --project $resolvedProjectPath --no-launch-profile
|
||||
} else {
|
||||
Write-Host " - launch profile: $LaunchProfile"
|
||||
dotnet run --project $resolvedProjectPath --launch-profile $LaunchProfile
|
||||
}
|
||||
45
OpenJibo/scripts/cloud/Start-OpenJiboNode.ps1
Normal file
45
OpenJibo/scripts/cloud/Start-OpenJiboNode.ps1
Normal file
@@ -0,0 +1,45 @@
|
||||
param(
|
||||
[string]$NodeDirectory = "src/Jibo.Cloud/node",
|
||||
[switch]$Install
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\.."))
|
||||
$resolvedNodeDirectory = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $NodeDirectory))
|
||||
$serverPath = Join-Path $resolvedNodeDirectory "open-jibo-link.js"
|
||||
$packagePath = Join-Path $resolvedNodeDirectory "package.json"
|
||||
$certPath = Join-Path $resolvedNodeDirectory "cert.pem"
|
||||
$keyPath = Join-Path $resolvedNodeDirectory "key.pem"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $serverPath)) {
|
||||
throw "Could not find Node server at $serverPath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $packagePath)) {
|
||||
throw "Could not find package.json at $packagePath"
|
||||
}
|
||||
|
||||
if ($Install -or -not (Test-Path -LiteralPath (Join-Path $resolvedNodeDirectory "node_modules"))) {
|
||||
Write-Host "Installing Node dependencies"
|
||||
npm install --prefix $resolvedNodeDirectory
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $certPath) -or -not (Test-Path -LiteralPath $keyPath)) {
|
||||
Write-Warning "cert.pem and key.pem are not present in $resolvedNodeDirectory."
|
||||
Write-Warning "The Node oracle expects those files because it binds HTTPS on port 443."
|
||||
Write-Warning "Use the same dev certificate material that your controlled Jibo routing already trusts."
|
||||
}
|
||||
|
||||
Write-Host "Starting OpenJibo Node protocol oracle"
|
||||
Write-Host " - directory: $resolvedNodeDirectory"
|
||||
Write-Host " - server: $serverPath"
|
||||
Write-Host " - port: 443"
|
||||
|
||||
Push-Location $resolvedNodeDirectory
|
||||
try {
|
||||
node .\open-jibo-link.js
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
20
OpenJibo/scripts/cloud/Start-OpenJiboPlayground.ps1
Normal file
20
OpenJibo/scripts/cloud/Start-OpenJiboPlayground.ps1
Normal file
@@ -0,0 +1,20 @@
|
||||
param(
|
||||
[string]$ProjectPath = "src/Playground/Playground.csproj"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\.."))
|
||||
$resolvedProjectPath = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $ProjectPath))
|
||||
|
||||
if (-not (Test-Path -LiteralPath $resolvedProjectPath)) {
|
||||
throw "Could not find Playground project at $resolvedProjectPath"
|
||||
}
|
||||
|
||||
Write-Host "Starting OpenJibo Playground"
|
||||
Write-Host " - project: $resolvedProjectPath"
|
||||
Write-Host " - mode: direct local Jibo ASR/TTS client"
|
||||
Write-Host ""
|
||||
Write-Host "When prompted, enter the Jibo IP address on your local network."
|
||||
|
||||
dotnet run --project $resolvedProjectPath
|
||||
@@ -70,6 +70,10 @@ For a real storage account, swap `UseDevelopmentStorage=true` with your Azure St
|
||||
|
||||
## Local Startup Note
|
||||
|
||||
For the practical local run guide, including `.NET`, Node, and Playground, start with:
|
||||
|
||||
- [Local OpenJibo Cloud Quickstart](../../docs/local-cloud-quickstart.md)
|
||||
|
||||
To run the API with the Blob-backed sample config in Visual Studio or `dotnet run`, choose the
|
||||
`Jibo.Cloud.Api.AzureBlob` launch profile.
|
||||
|
||||
|
||||
@@ -8,6 +8,15 @@ This is the production-oriented path for restoring device connectivity and creat
|
||||
|
||||
Current spoken cloud version: `Cloud version 1.0.19.`
|
||||
|
||||
Local startup:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboDotNet.ps1
|
||||
```
|
||||
|
||||
Run that from the repo root. For the full local guide, including Node and Playground, see
|
||||
[local-cloud-quickstart.md](../../../docs/local-cloud-quickstart.md).
|
||||
|
||||
Release hygiene reminder:
|
||||
|
||||
- bump [OpenJiboCloudBuildInfo.cs](/C:/Projects/JiboExperiments/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/OpenJiboCloudBuildInfo.cs) whenever we ship a meaningful hosted-cloud update
|
||||
|
||||
@@ -239,4 +239,6 @@ static CloudSession ResolveSession(JiboWebSocketService webSocketService, WebSoc
|
||||
return webSocketService.GetOrCreateSession(envelope);
|
||||
}
|
||||
|
||||
internal sealed record ReceivedSocketMessage(WebSocketMessageType MessageType, byte[] Buffer);
|
||||
internal sealed record ReceivedSocketMessage(WebSocketMessageType MessageType, byte[] Buffer);
|
||||
|
||||
public partial class Program;
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class CloudStateCommuteReportProvider(ICloudStateStore cloudStateS
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? ResolveLoopId(TurnContext turn)
|
||||
private static string ResolveLoopId(TurnContext turn)
|
||||
{
|
||||
if (turn.Attributes.TryGetValue("loopId", out var loopValue) &&
|
||||
loopValue is not null &&
|
||||
@@ -147,4 +147,4 @@ public sealed class CloudStateCommuteReportProvider(ICloudStateStore cloudStateS
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,22 @@ internal sealed class JsonFileSnapshotStore(string? persistencePath, JsonSeriali
|
||||
var directory = Path.GetDirectoryName(persistencePath);
|
||||
if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);
|
||||
|
||||
File.WriteAllText(persistencePath, JsonSerializer.Serialize(snapshot, options));
|
||||
var tempPath = Path.Combine(
|
||||
string.IsNullOrWhiteSpace(directory) ? Directory.GetCurrentDirectory() : directory,
|
||||
$".{Path.GetFileName(persistencePath)}.{Guid.NewGuid():N}.tmp");
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(snapshot, options));
|
||||
|
||||
if (File.Exists(persistencePath))
|
||||
File.Replace(tempPath, persistencePath, null);
|
||||
else
|
||||
File.Move(tempPath, persistencePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath)) File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,28 @@ The Node server is still the best place to:
|
||||
|
||||
It is no longer the intended production runtime.
|
||||
|
||||
## Local Startup
|
||||
|
||||
From the repo root:
|
||||
|
||||
```powershell
|
||||
.\scripts\cloud\Start-OpenJiboNode.ps1 -Install
|
||||
```
|
||||
|
||||
Manual equivalent:
|
||||
|
||||
```powershell
|
||||
cd src\Jibo.Cloud\node
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
The Node oracle binds HTTPS on port `443` and expects `cert.pem` and `key.pem` in this folder.
|
||||
Use the same certificate material that your controlled Jibo routing already trusts.
|
||||
|
||||
For the full local guide, including the current `.NET` cloud and Playground, see
|
||||
[local-cloud-quickstart.md](../../../docs/local-cloud-quickstart.md).
|
||||
|
||||
## What Stays Here
|
||||
|
||||
- reverse-engineering work
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"description": "",
|
||||
"main": "open-jibo-link.js",
|
||||
"scripts": {
|
||||
"start": "node open-jibo-link.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
namespace Jibo.Cloud.Tests.Api;
|
||||
|
||||
public sealed class JiboCloudApiIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Health_ReturnsCurrentVersion()
|
||||
{
|
||||
await using var factory = CreateFactory();
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
var body = await response.Content.ReadFromJsonAsync<HealthResponse>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(body);
|
||||
Assert.True(body!.Ok);
|
||||
Assert.Equal("OpenJibo Cloud Api", body.Service);
|
||||
Assert.Equal("1.0.19", body.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HttpProtocolDispatch_HandlesCreateHubTokenTarget()
|
||||
{
|
||||
await using var factory = CreateFactory();
|
||||
var client = factory.CreateClient();
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/")
|
||||
{
|
||||
Content = JsonContent.Create(new { })
|
||||
};
|
||||
request.Headers.TryAddWithoutValidation("X-Amz-Target", "Account_20160715.CreateHubToken");
|
||||
request.Headers.Host = "api.jibo.com";
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
var payload = await response.Content.ReadFromJsonAsync<CreateHubTokenResponse>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.NotNull(payload);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload!.Token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WebSocket_MissingTokenOnNeoHubListen_ReturnsUnauthorized()
|
||||
{
|
||||
await using var factory = CreateFactory();
|
||||
var client = factory.Server.CreateWebSocketClient();
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
client.ConnectAsync(new Uri("ws://neo-hub.jibo.com/"), CancellationToken.None));
|
||||
|
||||
Assert.Contains("401", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WebSocket_TokenPathOnNeoHubListen_Connects()
|
||||
{
|
||||
await using var factory = CreateFactory();
|
||||
var client = factory.Server.CreateWebSocketClient();
|
||||
|
||||
using var socket = await client.ConnectAsync(new Uri("ws://neo-hub.jibo.com/test-token"), CancellationToken.None);
|
||||
|
||||
Assert.Equal(WebSocketState.Open, socket.State);
|
||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "test-complete", CancellationToken.None);
|
||||
}
|
||||
|
||||
private static WebApplicationFactory<Program> CreateFactory()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), $"openjibo-api-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(root);
|
||||
|
||||
return new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.UseSetting("OpenJibo:Telemetry:DirectoryPath", Path.Combine(root, "websocket"));
|
||||
builder.UseSetting("OpenJibo:ProtocolTelemetry:DirectoryPath", Path.Combine(root, "http"));
|
||||
builder.UseSetting("OpenJibo:TurnTelemetry:DirectoryPath", Path.Combine(root, "turn"));
|
||||
builder.UseSetting("OpenJibo:State:PersistencePath", Path.Combine(root, "cloud-state.json"));
|
||||
builder.UseSetting("OpenJibo:PersonalMemory:PersistencePath", Path.Combine(root, "personal-memory.json"));
|
||||
builder.UseSetting("OpenJibo:Media:DirectoryPath", Path.Combine(root, "media"));
|
||||
});
|
||||
}
|
||||
|
||||
private sealed record HealthResponse(bool Ok, string Service, string Version);
|
||||
|
||||
private sealed record CreateHubTokenResponse(string Token);
|
||||
}
|
||||
@@ -190,6 +190,34 @@ public sealed class PersistenceStoreTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersonalMemoryStore_IgnoresCorruptSnapshotAndOverwritesWithValidJson()
|
||||
{
|
||||
var persistenceDirectory = Path.Combine(Path.GetTempPath(), $"openjibo-corrupt-memory-{Guid.NewGuid():N}");
|
||||
var persistencePath = Path.Combine(persistenceDirectory, "memory.json");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(persistenceDirectory);
|
||||
File.WriteAllText(persistencePath, "{ not valid json");
|
||||
|
||||
var scope = new PersonalMemoryTenantScope("acct-corrupt", "loop-corrupt", "device-corrupt");
|
||||
var store = new InMemoryPersonalMemoryStore(persistencePath);
|
||||
Assert.Null(store.GetName(scope));
|
||||
|
||||
store.SetName(scope, "Recovered");
|
||||
|
||||
var reloaded = new InMemoryPersonalMemoryStore(persistencePath);
|
||||
Assert.Equal("Recovered", reloaded.GetName(scope));
|
||||
Assert.DoesNotContain(Directory.GetFiles(persistenceDirectory),
|
||||
path => Path.GetFileName(path).Contains(".tmp", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(persistenceDirectory)) Directory.Delete(persistenceDirectory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingSnapshotStore : ISnapshotStore
|
||||
{
|
||||
public List<object> Saves { get; } = [];
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Api\Jibo.Cloud.Api.csproj" />
|
||||
<ProjectReference Include="..\..\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Application\Jibo.Cloud.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\Jibo.Cloud\dotnet\src\Jibo.Cloud.Infrastructure\Jibo.Cloud.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\src\Jibo.Runtime.Abstractions\Jibo.Runtime.Abstractions.csproj" />
|
||||
|
||||
@@ -333,7 +333,7 @@ public sealed class JiboInteractionServiceTests
|
||||
["messageType"] = "TRIGGER",
|
||||
["triggerSource"] = "PRESENCE",
|
||||
["context"] =
|
||||
"""{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
|
||||
"""{"runtime":{"location":{"iso":"2026-05-21T15:00:00-05:00"},"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
|
||||
}
|
||||
});
|
||||
|
||||
@@ -364,7 +364,7 @@ public sealed class JiboInteractionServiceTests
|
||||
["messageType"] = "TRIGGER",
|
||||
["triggerSource"] = "PRESENCE",
|
||||
["context"] =
|
||||
"""{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"},{"id":"person-2"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"},{"id":"person-2","firstName":"sam"}]}}}"""
|
||||
"""{"runtime":{"location":{"iso":"2026-05-21T15:00:00-05:00"},"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"},{"id":"person-2"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"},{"id":"person-2","firstName":"sam"}]}}}"""
|
||||
}
|
||||
});
|
||||
|
||||
@@ -513,6 +513,58 @@ public sealed class JiboInteractionServiceTests
|
||||
Assert.Equal("ProactiveHolidayGreeting", decision.ContextUpdates![GreetingRouteKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_TriggerUsesHolidayGreetingOnlyOnMatchingFixedDate()
|
||||
{
|
||||
var cloudStateStore = new InMemoryCloudStateStore();
|
||||
cloudStateStore.UpsertHoliday(new HolidayRecord
|
||||
{
|
||||
LoopId = "loop-fixed-holiday",
|
||||
Name = "Test Holiday",
|
||||
Category = "holiday",
|
||||
Date = new DateOnly(2026, 8, 13),
|
||||
IsEnabled = true,
|
||||
Source = "manual",
|
||||
CountryCode = "US"
|
||||
});
|
||||
var service = CreateService(cloudStateStore: cloudStateStore);
|
||||
|
||||
var ordinaryDecision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = string.Empty,
|
||||
NormalizedTranscript = string.Empty,
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
["accountId"] = "acct-fixed-holiday",
|
||||
["loopId"] = "loop-fixed-holiday",
|
||||
["messageType"] = "TRIGGER",
|
||||
["triggerSource"] = "PRESENCE",
|
||||
["context"] =
|
||||
"""{"runtime":{"location":{"iso":"2026-08-12T09:00:00-05:00"},"perception":{"speaker":"person-8","peoplePresent":[{"id":"person-8"}]},"loop":{"users":[{"id":"person-8","firstName":"jake"}]}}}"""
|
||||
}
|
||||
});
|
||||
|
||||
var holidayDecision = await service.BuildDecisionAsync(new TurnContext
|
||||
{
|
||||
RawTranscript = string.Empty,
|
||||
NormalizedTranscript = string.Empty,
|
||||
Attributes = new Dictionary<string, object?>
|
||||
{
|
||||
["accountId"] = "acct-fixed-holiday",
|
||||
["loopId"] = "loop-fixed-holiday",
|
||||
["messageType"] = "TRIGGER",
|
||||
["triggerSource"] = "PRESENCE",
|
||||
["context"] =
|
||||
"""{"runtime":{"location":{"iso":"2026-08-13T09:00:00-05:00"},"perception":{"speaker":"person-9","peoplePresent":[{"id":"person-9"}]},"loop":{"users":[{"id":"person-9","firstName":"sam"}]}}}"""
|
||||
}
|
||||
});
|
||||
|
||||
Assert.Equal("proactive_greeting", ordinaryDecision.IntentName);
|
||||
Assert.Equal("ProactiveGreeting", ordinaryDecision.ContextUpdates![GreetingRouteKey]);
|
||||
Assert.Equal("proactive_holiday_greeting", holidayDecision.IntentName);
|
||||
Assert.Equal("ProactiveHolidayGreeting", holidayDecision.ContextUpdates![GreetingRouteKey]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_SuppressesRepeatGreetingFromCloudHistory()
|
||||
{
|
||||
@@ -2255,7 +2307,9 @@ public sealed class JiboInteractionServiceTests
|
||||
|
||||
Assert.Equal("personal_report_opt_in", decision.IntentName);
|
||||
Assert.Equal("Would you like your personal report now?", decision.ReplyText);
|
||||
Assert.Equal("shared/yes_no", ((IReadOnlyList<string>)decision.SkillPayload!["listen_contexts"])[0]);
|
||||
Assert.NotNull(decision.SkillPayload);
|
||||
var listenContexts = Assert.IsAssignableFrom<IReadOnlyList<string>>(decision.SkillPayload["listen_contexts"]);
|
||||
Assert.Equal("shared/yes_no", listenContexts[0]);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("awaiting_opt_in", decision.ContextUpdates![PersonalReportStateKey]);
|
||||
Assert.Equal(true, decision.ContextUpdates[PersonalReportWeatherEnabledKey]);
|
||||
@@ -2286,7 +2340,9 @@ public sealed class JiboInteractionServiceTests
|
||||
|
||||
Assert.Equal("personal_report_verify_user", decision.IntentName);
|
||||
Assert.Equal("I think this is alex. Is that right?", decision.ReplyText);
|
||||
Assert.Equal("shared/yes_no", ((IReadOnlyList<string>)decision.SkillPayload!["listen_contexts"])[0]);
|
||||
Assert.NotNull(decision.SkillPayload);
|
||||
var listenContexts = Assert.IsAssignableFrom<IReadOnlyList<string>>(decision.SkillPayload["listen_contexts"]);
|
||||
Assert.Equal("shared/yes_no", listenContexts[0]);
|
||||
Assert.NotNull(decision.ContextUpdates);
|
||||
Assert.Equal("awaiting_identity_confirmation", decision.ContextUpdates![PersonalReportStateKey]);
|
||||
Assert.Equal("alex", decision.ContextUpdates[PersonalReportUserNameKey]);
|
||||
|
||||
Reference in New Issue
Block a user