diff --git a/.gitignore b/.gitignore index ff292d7..a8cfcb7 100644 --- a/.gitignore +++ b/.gitignore @@ -417,6 +417,7 @@ FodyWeavers.xsd **/.dotnet/ # OpenJibo live-run capture output +captures/ OpenJibo/captures/ OpenJibo/.tmp/ diff --git a/OpenJibo/README.md b/OpenJibo/README.md index 1123416..9636fc8 100644 --- a/OpenJibo/README.md +++ b/OpenJibo/README.md @@ -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) diff --git a/OpenJibo/docs/live-jibo-capture.md b/OpenJibo/docs/live-jibo-capture.md index 9431f9c..f20e346 100644 --- a/OpenJibo/docs/live-jibo-capture.md +++ b/OpenJibo/docs/live-jibo-capture.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. diff --git a/OpenJibo/docs/live-jibo-test-runbook.md b/OpenJibo/docs/live-jibo-test-runbook.md index eefb1c4..3e66532 100644 --- a/OpenJibo/docs/live-jibo-test-runbook.md +++ b/OpenJibo/docs/live-jibo-test-runbook.md @@ -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 diff --git a/OpenJibo/docs/protocol-inventory.md b/OpenJibo/docs/protocol-inventory.md index 4c5a06b..ed3f418 100644 --- a/OpenJibo/docs/protocol-inventory.md +++ b/OpenJibo/docs/protocol-inventory.md @@ -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: diff --git a/OpenJibo/scripts/cloud/README.md b/OpenJibo/scripts/cloud/README.md index 027fc3c..f8b82b4 100644 --- a/OpenJibo/scripts/cloud/README.md +++ b/OpenJibo/scripts/cloud/README.md @@ -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` diff --git a/OpenJibo/scripts/cloud/start-dotnet-with-node-cert.sh b/OpenJibo/scripts/cloud/start-dotnet-with-node-cert.sh index c404c15..f443bd8 100755 --- a/OpenJibo/scripts/cloud/start-dotnet-with-node-cert.sh +++ b/OpenJibo/scripts/cloud/start-dotnet-with-node-cert.sh @@ -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 diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/README.md b/OpenJibo/src/Jibo.Cloud/dotnet/README.md index 53b36ab..b408be9 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/README.md +++ b/OpenJibo/src/Jibo.Cloud/dotnet/README.md @@ -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 diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs index 12ebd6f..d74cd2a 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs @@ -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); diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Properties/AssemblyInfo.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3307944 --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Jibo.Cloud.Tests")] diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/CapturePathResolver.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/CapturePathResolver.cs new file mode 100644 index 0000000..48c67d5 --- /dev/null +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/CapturePathResolver.cs @@ -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; + } +} diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileProtocolTelemetrySink.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileProtocolTelemetrySink.cs index 8e916b8..a11bb6a 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileProtocolTelemetrySink.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileProtocolTelemetrySink.cs @@ -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"); diff --git a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileWebSocketTelemetrySink.cs b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileWebSocketTelemetrySink.cs index fc9609f..e7bb10e 100644 --- a/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileWebSocketTelemetrySink.cs +++ b/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Infrastructure/Telemetry/FileWebSocketTelemetrySink.cs @@ -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) diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/FileProtocolTelemetrySinkTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/FileProtocolTelemetrySinkTests.cs new file mode 100644 index 0000000..808395f --- /dev/null +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/FileProtocolTelemetrySinkTests.cs @@ -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.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); + } + } +} diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs index 8e70a7d..5837817 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/Protocol/JiboCloudProtocolServiceTests.cs @@ -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"}""" }); diff --git a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/FileWebSocketTelemetrySinkTests.cs b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/FileWebSocketTelemetrySinkTests.cs index 651d48f..9905402 100644 --- a/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/FileWebSocketTelemetrySinkTests.cs +++ b/OpenJibo/tests/Jibo.Cloud.Tests/WebSockets/FileWebSocketTelemetrySinkTests.cs @@ -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.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(