fixes for testing Jibo
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -417,6 +417,7 @@ FodyWeavers.xsd
|
||||
**/.dotnet/
|
||||
|
||||
# OpenJibo live-run capture output
|
||||
captures/
|
||||
OpenJibo/captures/
|
||||
OpenJibo/.tmp/
|
||||
|
||||
|
||||
@@ -78,10 +78,27 @@ OpenJibo/
|
||||
|
||||
- port required endpoint and WebSocket behavior from Node to .NET
|
||||
- keep protocol captures and replay fixtures current
|
||||
- keep HTTP and websocket live-run telemetry writing to the same repo-root capture tree
|
||||
- harden device bootstrap documentation and scripts
|
||||
- map more endpoints and behaviors beyond the current Node coverage
|
||||
- stand up the initial `openjibo.com` information site
|
||||
|
||||
## Live Test Status
|
||||
|
||||
The first physical `.NET -> Jibo` experiments have now produced useful captures, but not a full wake-and-interact success yet.
|
||||
|
||||
What we have confirmed so far:
|
||||
|
||||
- the robot reaches `.NET` HTTP startup calls on `api.jibo.com`
|
||||
- `.NET` can issue a robot token and accept the `api-socket.jibo.com` websocket
|
||||
- live HTTP and websocket telemetry are now intended to land together under repo-root `captures/`
|
||||
|
||||
What remains unresolved:
|
||||
|
||||
- matching the Node startup cadence closely enough for consistent wake/eye-open behavior
|
||||
- the next post-`api-socket` startup requests and timing seen in successful Node runs
|
||||
- broader live websocket behavior on a real robot beyond the current synthetic parity slice
|
||||
|
||||
## Important Docs
|
||||
|
||||
- [Cloud overview](/src/Jibo.Cloud/README.md)
|
||||
|
||||
@@ -36,27 +36,31 @@ Move to a second local/staging server or Azure after:
|
||||
|
||||
## Telemetry Before Live Runs
|
||||
|
||||
The `.NET` cloud now supports structured websocket capture intended for first live runs:
|
||||
The `.NET` cloud now supports structured live capture intended for first robot runs:
|
||||
|
||||
- event stream written as NDJSON
|
||||
- per-session fixture export for replay
|
||||
- HTTP request/response event streams written as NDJSON
|
||||
- websocket event streams written as NDJSON
|
||||
- per-session websocket fixture export for replay
|
||||
- turn metadata including `transID`, buffered audio counts, finalize attempts, and reply types
|
||||
|
||||
Default capture location:
|
||||
|
||||
- repo-root `captures/http/`
|
||||
- repo-root `captures/websocket/`
|
||||
|
||||
Artifacts:
|
||||
|
||||
- `http/*.events.ndjson`
|
||||
- `websocket/*.events.ndjson`
|
||||
- `*.events.ndjson`
|
||||
- `fixtures/*.flow.json`
|
||||
- `websocket/fixtures/*.flow.json`
|
||||
|
||||
## Suggested First Hookup Plan
|
||||
|
||||
1. Start the `.NET` API on the Ubuntu-backed controlled network using the same robot routing settings currently used for Node.
|
||||
2. Confirm HTTP bootstrap and websocket acceptance with the existing smoke/routing helpers.
|
||||
3. Run one or two controlled listen turns with Jibo.
|
||||
4. Inspect the captured websocket events and exported fixtures.
|
||||
4. Inspect the captured HTTP and websocket events plus exported websocket fixtures.
|
||||
5. Convert the best captures into sanitized checked-in fixtures and tests.
|
||||
6. Keep Node available to compare any surprising turn behavior before changing infrastructure.
|
||||
|
||||
|
||||
@@ -109,6 +109,11 @@ BASEURL=http://localhost:24605 ./scripts/cloud/invoke-live-jibo-prep.sh
|
||||
|
||||
- `captures/websocket/fixtures/`
|
||||
|
||||
Telemetry from the same run should also now be present under:
|
||||
|
||||
- `captures/http/`
|
||||
- `captures/websocket/`
|
||||
|
||||
9. Import the best fixture into the checked-in websocket fixture set:
|
||||
|
||||
```bash
|
||||
@@ -133,7 +138,8 @@ If the robot does not connect or the first turn fails:
|
||||
2. confirm the cert presented by the `.NET` API matches the currently working Node cert path
|
||||
3. confirm the Ubuntu routing still points Jibo traffic at the same machine
|
||||
4. compare the `.NET` websocket capture output with prior Node logs
|
||||
5. temporarily switch back to Node to confirm the environment still works
|
||||
5. compare the `.NET` HTTP capture output with prior Node logs
|
||||
6. temporarily switch back to Node to confirm the environment still works
|
||||
|
||||
## Not In Scope For This First Test
|
||||
|
||||
|
||||
@@ -45,11 +45,11 @@ Observed from `open-jibo-link.js`:
|
||||
| `Loop_*` | `List`, `ListLoops` | medium | initial dispatch implemented |
|
||||
| `Robot_*` | `GetRobot`, `UpdateRobot` | medium | initial dispatch implemented |
|
||||
| `Update_*` | `ListUpdates`, `ListUpdatesFrom`, `GetUpdateFrom`, `CreateUpdate`, `RemoveUpdate` | medium | list/get scaffolding implemented |
|
||||
| `Media_20160725` | `List`, `Get`, `Create`, `Remove` | medium | not yet ported |
|
||||
| `Log_*` | `PutEvents`, `PutEventsAsync`, `PutBinaryAsync`, `PutAsrBinary` | medium | upload endpoints reserved; detailed handling pending |
|
||||
| `Key_*` | `ShouldCreate`, `CreateSymmetricKey`, `GetRequest` | medium | pending |
|
||||
| `Person_*` | `ListHolidays` | low | pending |
|
||||
| `Backup_*` | `List` | low | pending |
|
||||
| `Media_20160725` | `List`, `Get`, `Create`, `Remove` | medium | implemented in current parity scaffold |
|
||||
| `Log_*` | `PutEvents`, `PutEventsAsync`, `PutBinaryAsync`, `PutAsrBinary` | medium | async upload metadata and placeholder upload endpoints implemented |
|
||||
| `Key_*` | `ShouldCreate`, `CreateSymmetricKey`, `GetRequest` | medium | implemented in current parity scaffold |
|
||||
| `Person_*` | `ListHolidays` | low | implemented in current parity scaffold |
|
||||
| `Backup_*` | `List` | low | implemented in current parity scaffold |
|
||||
|
||||
## WebSocket Flows
|
||||
|
||||
@@ -101,6 +101,16 @@ That separation is intentional. The synthetic STT path currently exists only to
|
||||
| `/upload/log-events` | async log upload target | medium | placeholder endpoint accepted |
|
||||
| `/upload/log-binary` | async binary upload target | medium | placeholder endpoint accepted |
|
||||
|
||||
## First Live .NET Capture Findings
|
||||
|
||||
The first real `.NET` robot run has confirmed only an early startup slice so far:
|
||||
|
||||
- `api.jibo.com` startup HTTP requests are reaching the `.NET` cloud
|
||||
- `Notification.NewRobotToken` is active in the robot startup sequence
|
||||
- `api-socket.jibo.com/{token}` is being accepted live
|
||||
|
||||
The first live run has not yet shown full startup parity with the working Node server. In particular, the successful Node run continues into additional health/log cadence after token issuance and socket acceptance, while the current `.NET` run has not yet reproduced that full progression consistently.
|
||||
|
||||
## First Core Revive Slice
|
||||
|
||||
The first .NET hosted milestone should fully support:
|
||||
|
||||
@@ -10,6 +10,8 @@ These scripts help exercise the new .NET hosted cloud locally.
|
||||
Summarizes captured websocket telemetry events and exported live-run fixtures from the .NET cloud.
|
||||
- repo-root `captures/http/`
|
||||
Structured HTTP request/response telemetry for live robot startup comparison.
|
||||
- repo-root `captures/websocket/`
|
||||
Structured websocket telemetry plus exported replay fixtures for live robot sessions.
|
||||
- `Invoke-LiveJiboPrep.ps1`
|
||||
Runs a small readiness checklist before the first physical Jibo test against the .NET cloud.
|
||||
- `Import-WebSocketCaptureFixture.ps1`
|
||||
|
||||
@@ -13,9 +13,11 @@ PFX_PASSWORD="${PFX_PASSWORD:-openjibo-dev-password}"
|
||||
ASPNETCORE_URLS="${ASPNETCORE_URLS:-https://0.0.0.0:443;http://0.0.0.0:24605}"
|
||||
DOTNET_ENVIRONMENT="${DOTNET_ENVIRONMENT:-Development}"
|
||||
CAPTURE_DIRECTORY="${CAPTURE_DIRECTORY:-${REPO_ROOT}/captures/websocket}"
|
||||
PROTOCOL_CAPTURE_DIRECTORY="${PROTOCOL_CAPTURE_DIRECTORY:-${REPO_ROOT}/captures/http}"
|
||||
|
||||
mkdir -p "$(dirname "${PFX_OUT}")"
|
||||
mkdir -p "${CAPTURE_DIRECTORY}"
|
||||
mkdir -p "${PROTOCOL_CAPTURE_DIRECTORY}"
|
||||
|
||||
if [[ ! -f "${CERT_PEM}" ]]; then
|
||||
echo "Missing CERT_PEM: ${CERT_PEM}" >&2
|
||||
@@ -59,13 +61,15 @@ export DOTNET_ENVIRONMENT
|
||||
export ASPNETCORE_Kestrel__Certificates__Default__Path="${PFX_OUT}"
|
||||
export ASPNETCORE_Kestrel__Certificates__Default__Password="${PFX_PASSWORD}"
|
||||
export OpenJibo__Telemetry__DirectoryPath="${CAPTURE_DIRECTORY}"
|
||||
export OpenJibo__ProtocolTelemetry__DirectoryPath="${PROTOCOL_CAPTURE_DIRECTORY}"
|
||||
|
||||
echo ""
|
||||
echo "Starting OpenJibo .NET cloud"
|
||||
echo " - project: ${API_PROJECT}"
|
||||
echo " - urls: ${ASPNETCORE_URLS}"
|
||||
echo " - environment: ${DOTNET_ENVIRONMENT}"
|
||||
echo " - captures: ${CAPTURE_DIRECTORY}"
|
||||
echo " - websocket captures: ${CAPTURE_DIRECTORY}"
|
||||
echo " - http captures: ${PROTOCOL_CAPTURE_DIRECTORY}"
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
exec dotnet run --project "${API_PROJECT}" --no-launch-profile
|
||||
|
||||
@@ -76,6 +76,7 @@ Current websocket scope is still intentionally narrow:
|
||||
- `CONTEXT` capture and follow-up turn state
|
||||
- `EOS` completion
|
||||
- first skill vertical for joke/chat `SKILL_ACTION` playback
|
||||
- repo-root live-run capture support for both `captures/http/` and `captures/websocket/`
|
||||
|
||||
Not yet covered:
|
||||
|
||||
@@ -85,3 +86,17 @@ Not yet covered:
|
||||
- upstream Nimbus or broader skill lifecycle behavior
|
||||
- animation / expression command families
|
||||
- ESML feature parity beyond the narrow synthetic playback payloads used in the current scaffold
|
||||
|
||||
## Live Capture Status
|
||||
|
||||
The first real `.NET` robot test has confirmed:
|
||||
|
||||
- startup HTTP traffic reaches the `.NET` cloud
|
||||
- `Notification.NewRobotToken` is in the active startup path
|
||||
- `api-socket.jibo.com` connections are being accepted live
|
||||
|
||||
It has not yet confirmed:
|
||||
|
||||
- full startup parity with the successful Node run cadence
|
||||
- consistent eye-open / wake completion on the robot
|
||||
- the later health/log upload sequence currently seen in the working Node run
|
||||
|
||||
@@ -278,8 +278,9 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
|
||||
}
|
||||
|
||||
var body = envelope.TryParseBody();
|
||||
var deviceId = envelope.DeviceId
|
||||
?? ReadString(body, "deviceId")
|
||||
var deviceId = !string.IsNullOrWhiteSpace(envelope.DeviceId)
|
||||
? envelope.DeviceId!
|
||||
: ReadString(body, "deviceId")
|
||||
?? ReadString(body, "serial_number")
|
||||
?? ReadString(body, "serialNumber")
|
||||
?? ReadString(body, "cpuid")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Jibo.Cloud.Tests")]
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Jibo.Cloud.Infrastructure.Telemetry;
|
||||
|
||||
internal static class CapturePathResolver
|
||||
{
|
||||
public static string Resolve(string configuredDirectoryPath, string currentDirectory, string appBaseDirectory)
|
||||
{
|
||||
if (Path.IsPathRooted(configuredDirectoryPath))
|
||||
{
|
||||
return Path.GetFullPath(configuredDirectoryPath);
|
||||
}
|
||||
|
||||
var repoRoot = FindOpenJiboRepoRoot(currentDirectory) ?? FindOpenJiboRepoRoot(appBaseDirectory);
|
||||
var baseDirectory = repoRoot ?? currentDirectory;
|
||||
return Path.GetFullPath(configuredDirectoryPath, baseDirectory);
|
||||
}
|
||||
|
||||
private static string? FindOpenJiboRepoRoot(string? startPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(startPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var directory = new DirectoryInfo(Path.GetFullPath(startPath));
|
||||
if (!directory.Exists && directory.Parent is not null)
|
||||
{
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "OpenJibo.slnx")))
|
||||
{
|
||||
return directory.FullName;
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,10 @@ public sealed class FileProtocolTelemetrySink(
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetFullPath(options.Value.DirectoryPath, AppContext.BaseDirectory);
|
||||
var directory = CapturePathResolver.Resolve(
|
||||
options.Value.DirectoryPath,
|
||||
Directory.GetCurrentDirectory(),
|
||||
AppContext.BaseDirectory);
|
||||
Directory.CreateDirectory(directory);
|
||||
var filePath = Path.Combine(directory, $"{DateTimeOffset.UtcNow:yyyyMMdd}.events.ndjson");
|
||||
|
||||
|
||||
@@ -227,7 +227,10 @@ public sealed class FileWebSocketTelemetrySink(
|
||||
|
||||
private string GetBaseDirectory()
|
||||
{
|
||||
return Path.GetFullPath(options.Value.DirectoryPath, AppContext.BaseDirectory);
|
||||
return CapturePathResolver.Resolve(
|
||||
options.Value.DirectoryPath,
|
||||
Directory.GetCurrentDirectory(),
|
||||
AppContext.BaseDirectory);
|
||||
}
|
||||
|
||||
private static string BuildFixtureName(CloudSession session, CapturedWebSocketFixtureBuilder fixture)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Jibo.Cloud.Domain.Models;
|
||||
using Jibo.Cloud.Infrastructure.Telemetry;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Jibo.Cloud.Tests.Protocol;
|
||||
|
||||
public sealed class FileProtocolTelemetrySinkTests : IDisposable
|
||||
{
|
||||
private readonly string _workspaceRoot;
|
||||
private readonly string _repoRoot;
|
||||
private readonly string _appBaseDirectory;
|
||||
|
||||
public FileProtocolTelemetrySinkTests()
|
||||
{
|
||||
_workspaceRoot = Path.Combine(Path.GetTempPath(), "OpenJibo.ProtocolTelemetry.Tests", Guid.NewGuid().ToString("N"));
|
||||
_repoRoot = Path.Combine(_workspaceRoot, "OpenJibo");
|
||||
_appBaseDirectory = Path.Combine(_repoRoot, "src", "Jibo.Cloud", "dotnet", "src", "Jibo.Cloud.Api", "bin", "Debug", "net10.0");
|
||||
|
||||
Directory.CreateDirectory(_repoRoot);
|
||||
Directory.CreateDirectory(_appBaseDirectory);
|
||||
File.WriteAllText(Path.Combine(_repoRoot, "OpenJibo.slnx"), string.Empty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecordAsync_ResolvesRelativePathAgainstOpenJiboRepoRoot()
|
||||
{
|
||||
var captureDirectory = CapturePathResolver.Resolve("captures/http", _repoRoot, _appBaseDirectory);
|
||||
var sink = new FileProtocolTelemetrySink(
|
||||
NullLogger<FileProtocolTelemetrySink>.Instance,
|
||||
Options.Create(new ProtocolTelemetryOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DirectoryPath = captureDirectory
|
||||
}));
|
||||
|
||||
var envelope = new ProtocolEnvelope
|
||||
{
|
||||
HostName = "api.jibo.com",
|
||||
Method = "POST",
|
||||
Path = "/",
|
||||
ServicePrefix = "Notification_20150505",
|
||||
Operation = "NewRobotToken",
|
||||
BodyText = """{"deviceId":"robot-123"}"""
|
||||
};
|
||||
|
||||
await sink.RecordAsync(envelope, ProtocolDispatchResult.Ok(new { token = "token-robot-123" }));
|
||||
|
||||
var captureFile = Directory.GetFiles(captureDirectory, "*.events.ndjson").Single();
|
||||
var contents = await File.ReadAllTextAsync(captureFile);
|
||||
|
||||
Assert.Contains("Notification_20150505", contents);
|
||||
Assert.DoesNotContain(Path.Combine("bin", "Debug"), captureFile, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_workspaceRoot))
|
||||
{
|
||||
Directory.Delete(_workspaceRoot, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public sealed class JiboCloudProtocolServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NewRobotToken_UsesBodyDeviceId()
|
||||
public async Task NewRobotToken_UsesBodyDeviceId_WhenHeaderDeviceIdIsEmpty()
|
||||
{
|
||||
var result = await _service.DispatchAsync(new ProtocolEnvelope
|
||||
{
|
||||
@@ -36,6 +36,7 @@ public sealed class JiboCloudProtocolServiceTests
|
||||
Method = "POST",
|
||||
ServicePrefix = "Notification_20160715",
|
||||
Operation = "NewRobotToken",
|
||||
DeviceId = string.Empty,
|
||||
BodyText = """{"deviceId":"robot-123"}"""
|
||||
});
|
||||
|
||||
|
||||
@@ -9,10 +9,14 @@ namespace Jibo.Cloud.Tests.WebSockets;
|
||||
public sealed class FileWebSocketTelemetrySinkTests : IDisposable
|
||||
{
|
||||
private readonly string _directoryPath;
|
||||
private readonly string _repoRoot;
|
||||
private readonly string _appBaseDirectory;
|
||||
|
||||
public FileWebSocketTelemetrySinkTests()
|
||||
{
|
||||
_directoryPath = Path.Combine(Path.GetTempPath(), "OpenJibo.Tests", Guid.NewGuid().ToString("N"));
|
||||
_repoRoot = Path.Combine(_directoryPath, "OpenJibo");
|
||||
_appBaseDirectory = Path.Combine(_repoRoot, "src", "Jibo.Cloud", "dotnet", "src", "Jibo.Cloud.Api", "bin", "Debug", "net10.0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -61,6 +65,48 @@ public sealed class FileWebSocketTelemetrySinkTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RecordsFixtureUsingRepoRootForRelativePaths()
|
||||
{
|
||||
Directory.CreateDirectory(_repoRoot);
|
||||
Directory.CreateDirectory(_appBaseDirectory);
|
||||
File.WriteAllText(Path.Combine(_repoRoot, "OpenJibo.slnx"), string.Empty);
|
||||
var captureDirectory = CapturePathResolver.Resolve("captures/websocket", _repoRoot, _appBaseDirectory);
|
||||
|
||||
var sink = new FileWebSocketTelemetrySink(
|
||||
NullLogger<FileWebSocketTelemetrySink>.Instance,
|
||||
Options.Create(new WebSocketTelemetryOptions
|
||||
{
|
||||
Enabled = true,
|
||||
ExportFixtures = true,
|
||||
DirectoryPath = captureDirectory
|
||||
}));
|
||||
|
||||
var envelope = new WebSocketMessageEnvelope
|
||||
{
|
||||
ConnectionId = "conn-relative",
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen",
|
||||
Kind = "neo-hub-listen",
|
||||
Token = "token-relative",
|
||||
Text = """{"type":"LISTEN","transID":"trans-relative","data":{"text":"hello"}}"""
|
||||
};
|
||||
var session = new CloudSession
|
||||
{
|
||||
Token = "token-relative",
|
||||
HostName = "neo-hub.jibo.com",
|
||||
Path = "/listen"
|
||||
};
|
||||
session.TurnState.TransId = "trans-relative";
|
||||
|
||||
await sink.RecordConnectionOpenedAsync(envelope, session);
|
||||
await sink.RecordOutboundAsync(envelope, session, [new WebSocketReply { Text = """{"type":"LISTEN"}""" }]);
|
||||
await sink.RecordConnectionClosedAsync(envelope, session, "test");
|
||||
|
||||
var fixtureDirectory = Path.Combine(captureDirectory, "fixtures");
|
||||
Assert.Single(Directory.GetFiles(fixtureDirectory, "*.flow.json"));
|
||||
}
|
||||
|
||||
private FileWebSocketTelemetrySink CreateSink()
|
||||
{
|
||||
return new FileWebSocketTelemetrySink(
|
||||
|
||||
Reference in New Issue
Block a user