From 5f80707c15d7d3d26947fde63e4d868c4eb2dff1 Mon Sep 17 00:00:00 2001 From: OttoBot Date: Mon, 23 Feb 2026 18:39:27 -0600 Subject: [PATCH] test: add ScalaPB codegen integration tests + CI + proto README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 4 missing integration tests (ArchiveStateMachine, CreateScript, InvokeScript, OttochainMessage union) to ScalaPBIntegrationTest.scala - Verify all 6 DataUpdate message types compile with mixin correctly - Fix CI: add proto/compile proto/test step before sharedData/test (proto module was previously invisible to CI) - Add modules/proto/README.md documenting build-time generation strategy, DataUpdate mixin integration, and ProtoAdapters Phase 1/2 boundary Spec: PR #95 (docs/design/scalapb-codegen-spec.md) TDD gate: all tests compile = DataUpdate mixin works for all 6 types ~1.5h @work effort (simple, per spec) Closes Trello card: https://trello.com/c/IMFiVKvx (⚙️ ScalaPB: Configure codegen) --- .github/workflows/ci.yml | 2 +- modules/proto/README.md | 145 ++++++++++++++++++ .../kd5ujc/proto/ScalaPBIntegrationTest.scala | 78 +++++++++- 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 modules/proto/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9e75964..18481e37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: ${{ runner.os }}-sbt- - name: Run tests with coverage - run: sbt clean coverage sharedData/test coverageReport + run: sbt clean coverage proto/compile proto/test sharedData/test coverageReport - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/modules/proto/README.md b/modules/proto/README.md new file mode 100644 index 00000000..6c8e5abb --- /dev/null +++ b/modules/proto/README.md @@ -0,0 +1,145 @@ +# OttoChain Proto Module + +This module provides Protocol Buffer definitions and ScalaPB-generated Scala types for OttoChain data structures. + +## Build-Time Generation Strategy + +### ScalaPB Configuration + +The module uses **build-time generation** via sbt-protoc and ScalaPB: + +```scala +Compile / PB.targets := Seq( + scalapb.gen(flatPackage = true) -> (Compile / sourceManaged).value / "scalapb", + scalapb.validate.gen() -> (Compile / sourceManaged).value / "scalapb" +) +``` + +**Generated files location:** `target/scala-2.13/src_managed/main/scalapb/` (gitignored) + +**Benefits:** +- Consistent with Tessellation's build strategy +- Generated types available at compile time +- No runtime protoc dependencies +- IDE integration works seamlessly + +### DataUpdate Mixin Integration + +All OttoChain message types extend `io.constellationnetwork.currency.dataApplication.DataUpdate` via ScalaPB options: + +```protobuf +message CreateStateMachine { + option (scalapb.message).extends = "io.constellationnetwork.currency.dataApplication.DataUpdate"; + // ... +} +``` + +This enables: +- Direct submission to Tessellation's DataL1 +- Type-safe metagraph integration +- Seamless ML0 processing + +## Type System Architecture + +### Proto vs. Scala Model Boundary + +**Key architectural decision:** Proto types and Scala domain models serve different purposes and coexist: + +- **Proto types** (`ottochain.v1.*`): Wire format, serialization, network transport +- **Scala models** (`xyz.kd5ujc.models.*`): Domain logic, business rules, type safety + +### ProtoAdapters Migration Strategy + +The `ProtoAdapters.scala` module handles conversion between proto and domain types: + +```scala +// Phase 1: Records conversion (outbound only) +def toProto(record: FiberRecord): ottochain.v1.FiberRecord = ... + +// Phase 2: Message conversion (inbound + outbound) - after PR #89 +def fromProto(msg: ottochain.v1.OttochainMessage): models.Transaction = ... +``` + +**Current status:** Phase 1 complete, Phase 2 planned post-fiber-engine migration. + +### Sequenced Trait Structural Gap + +Generated proto types have structural differences from hand-written Scala types: + +- **Generated:** `fiberId: String`, `targetSequenceNumber: Option[proto.FiberOrdinal]` +- **Domain models:** `fiberId: UUID`, `sequenceNumber: models.FiberOrdinal` + +The `Sequenced` trait cannot be directly implemented by generated types. ProtoAdapters bridges this gap during conversion. + +## Testing Strategy + +### Integration Tests + +`ScalaPBIntegrationTest.scala` verifies: +1. **Compilation:** Generated types compile successfully +2. **Mixin verification:** All message types extend `DataUpdate` +3. **Type safety:** Constructor and field access work correctly +4. **Union types:** `OttochainMessage` oneof functionality + +**Coverage:** 6 message types tested (CreateStateMachine, TransitionStateMachine, ArchiveStateMachine, CreateScript, InvokeScript, OttochainMessage) + +### CI Integration + +Proto module included in CI pipeline: +```bash +sbt proto/compile proto/test +``` + +This ensures: +- Proto definitions compile successfully +- Generated types satisfy type constraints +- Mixin injection works correctly +- No regressions in proto → Scala conversion + +## Proto Definitions + +### Core Messages + +| Message | Purpose | Key Fields | +|---------|---------|------------| +| `CreateStateMachine` | Create new fiber | `fiber_id`, `definition`, `initial_data` | +| `TransitionStateMachine` | Trigger state transition | `fiber_id`, `event_name`, `payload` | +| `ArchiveStateMachine` | Archive fiber | `fiber_id`, `target_sequence_number` | +| `CreateScript` | Create script fiber | `fiber_id`, `script_program`, `access_control` | +| `InvokeScript` | Execute script method | `fiber_id`, `method`, `args` | +| `OttochainMessage` | Union of all messages | `oneof message` | + +### Validation Rules + +Proto definitions include validation via protoc-gen-validate: +- `fiber_id`: minimum length 1 +- `event_name`: minimum length 1 +- `method`: minimum length 1 + +## Development Workflow + +### Adding New Message Types + +1. **Define proto message** in `src/main/protobuf/ottochain/v1/messages.proto` +2. **Add DataUpdate mixin** via `scalapb.message.extends` option +3. **Add to union** in `OttochainMessage.oneof` +4. **Write integration test** in `ScalaPBIntegrationTest.scala` +5. **Update ProtoAdapters** (if domain model exists) + +### Proto Schema Evolution + +**Field numbering policy:** +- Reserve ranges 1-100 for core fields +- Reserve 101-200 for extensions +- Never reuse field numbers +- Document breaking changes in migration guide + +### Generated Code Inspection + +Generated Scala files are in `target/scala-2.13/src_managed/main/scalapb/`. + +**Useful for debugging:** +- Verify mixin inheritance +- Check constructor signatures +- Inspect validation logic +- Understand union type structure \ No newline at end of file diff --git a/modules/proto/src/test/scala/xyz/kd5ujc/proto/ScalaPBIntegrationTest.scala b/modules/proto/src/test/scala/xyz/kd5ujc/proto/ScalaPBIntegrationTest.scala index ad252ec9..6d93dcd9 100644 --- a/modules/proto/src/test/scala/xyz/kd5ujc/proto/ScalaPBIntegrationTest.scala +++ b/modules/proto/src/test/scala/xyz/kd5ujc/proto/ScalaPBIntegrationTest.scala @@ -4,7 +4,7 @@ import cats.effect.IO import io.constellationnetwork.currency.dataApplication.DataUpdate -import ottochain.v1.{CreateStateMachine, TransitionStateMachine} +import ottochain.v1._ import weaver.SimpleIOSuite object ScalaPBIntegrationTest extends SimpleIOSuite { @@ -43,4 +43,80 @@ object ScalaPBIntegrationTest extends SimpleIOSuite { expect(dataUpdate.isInstanceOf[DataUpdate]) } } + + test("Generated ArchiveStateMachine extends DataUpdate") { + IO { + val archiveSM = ArchiveStateMachine( + fiberId = "test-archive-id", + targetSequenceNumber = Some(FiberOrdinal(value = 42)) + ) + + // This should compile successfully if DataUpdate mixin works + val dataUpdate: DataUpdate = archiveSM + + expect(archiveSM.fiberId == "test-archive-id") && + expect(archiveSM.targetSequenceNumber.exists(_.value == 42)) && + expect(dataUpdate.isInstanceOf[DataUpdate]) + } + } + + test("Generated CreateScript extends DataUpdate") { + IO { + val createScript = CreateScript( + fiberId = "test-script-id", + scriptProgram = None, + initialState = None, + accessControl = Some(AccessControlPolicy()) + ) + + // This should compile successfully if DataUpdate mixin works + val dataUpdate: DataUpdate = createScript + + expect(createScript.fiberId == "test-script-id") && + expect(createScript.accessControl.isDefined) && + expect(dataUpdate.isInstanceOf[DataUpdate]) + } + } + + test("Generated InvokeScript extends DataUpdate") { + IO { + val invokeScript = InvokeScript( + fiberId = "test-invoke-id", + method = "execute", + args = None, + targetSequenceNumber = Some(FiberOrdinal(value = 10)) + ) + + // This should compile successfully if DataUpdate mixin works + val dataUpdate: DataUpdate = invokeScript + + expect(invokeScript.fiberId == "test-invoke-id") && + expect(invokeScript.method == "execute") && + expect(invokeScript.targetSequenceNumber.exists(_.value == 10)) && + expect(dataUpdate.isInstanceOf[DataUpdate]) + } + } + + test("Generated OttochainMessage union extends DataUpdate") { + IO { + // Test with CreateStateMachine variant + val createSM = CreateStateMachine( + fiberId = "union-test-id", + definition = None, + initialData = None, + parentFiberId = None + ) + + val ottochainMessage = OttochainMessage( + message = OttochainMessage.Message.CreateStateMachine(createSM) + ) + + // This should compile successfully if DataUpdate mixin works + val dataUpdate: DataUpdate = ottochainMessage + + expect(ottochainMessage.message.isCreateStateMachine) && + expect(ottochainMessage.message.createStateMachine.exists(_.fiberId == "union-test-id")) && + expect(dataUpdate.isInstanceOf[DataUpdate]) + } + } }