fixes for testing Jibo

This commit is contained in:
Jacob Dubin
2026-04-15 11:58:58 -05:00
parent e7978b437a
commit 64ef8d61a0
16 changed files with 244 additions and 23 deletions

1
.gitignore vendored
View File

@@ -417,6 +417,7 @@ FodyWeavers.xsd
**/.dotnet/
# OpenJibo live-run capture output
captures/
OpenJibo/captures/
OpenJibo/.tmp/

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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:

View File

@@ -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`

View File

@@ -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

View File

@@ -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

View File

@@ -278,14 +278,15 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
}
var body = envelope.TryParseBody();
var deviceId = envelope.DeviceId
?? ReadString(body, "deviceId")
?? ReadString(body, "serial_number")
?? ReadString(body, "serialNumber")
?? ReadString(body, "cpuid")
?? ReadString(body, "cpuId")
?? ReadString(body, "robotId")
?? "unknown-device";
var deviceId = !string.IsNullOrWhiteSpace(envelope.DeviceId)
? envelope.DeviceId!
: ReadString(body, "deviceId")
?? ReadString(body, "serial_number")
?? ReadString(body, "serialNumber")
?? ReadString(body, "cpuid")
?? ReadString(body, "cpuId")
?? ReadString(body, "robotId")
?? "unknown-device";
stateStore.GetOrCreateDevice(deviceId, envelope.FirmwareVersion, envelope.ApplicationVersion);

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Jibo.Cloud.Tests")]

View File

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

View File

@@ -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");

View File

@@ -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)

View File

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

View File

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

View File

@@ -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(