core/plugin/wasmguest/bnwasm/tx_expired_test.go
Alex Dunmow ad6d87bf06 feat(ninja): plugin build/verify → .bnp packer + ABI riders (WO-WZ-009)
`ninja plugin build` compiles a plugin to reactor-mode wasip1 wasm (Go >= 1.24
enforced), extracts manifest.pb by driving one HOOK_DESCRIBE over wazero with
failing host stubs, and packs a tar.zst .bnp (plugin.wasm, plugin.mod,
manifest.pb + migrations/schemas/assets/web-dist when present) with a summary
table. A describe-time capability call (e.g. db.* from Register) fails with an
actionable error naming the offending method. `ninja plugin verify` re-runs the
CMS reader's layout/name/abi/path-safety/size checks standalone (deliberate
duplication of cms backend/plugin/bnp/reader.go; kept in lockstep by WO-WZ-010).

ABI riders (additive; buf breaking clean):
- ABI_ERROR_CODE_TX_EXPIRED enum value + bnwasm guest mapping to a new
  bnwasm.ErrTxExpired sentinel (retryable tx expiry, distinct from real faults);
  the cms dbexec side adopts the emit separately.
- PluginManifest.data_dir bool + a first-class `data_dir` key on the plugin.mod
  parser (so writeMod's struct round-trip can't drop it); `plugin build` stamps
  it from plugin.mod into the manifest.

Docs: wasm-abi.md gains a Building & packing section, the error-code table row,
the manifest data_dir mapping, and the plugin.mod reference. Tests: CLI e2e
builds the WZ-002 fixture → verify + manifest block keys; a capfixture proves
the actionable describe-time error; verify rejects each malformed class;
bnwasm TX_EXPIRED classification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:34:17 +08:00

75 lines
2.4 KiB
Go

package bnwasm
import (
"context"
"errors"
"testing"
abiv1 "git.dev.alexdunmow.com/block/core/abi/v1"
"google.golang.org/protobuf/proto"
)
// fakeHostErr mirrors *wasmguest.HostError's AbiErrorCode() method so the
// transport's cross-boundary classification (mapTransportErr) can be exercised
// natively, without importing the wasip1-only wasmguest package.
type fakeHostErr struct {
code abiv1.AbiErrorCode
msg string
}
func (e *fakeHostErr) Error() string { return e.msg }
func (e *fakeHostErr) AbiErrorCode() abiv1.AbiErrorCode { return e.code }
// transportReturning builds a Transport whose every call fails with err.
func transportReturning(err error) Transport {
return func(_ string, _, _ proto.Message) error { return err }
}
func TestTransportMapsTxExpiredToSentinel(t *testing.T) {
hostErr := &fakeHostErr{code: abiv1.AbiErrorCode_ABI_ERROR_CODE_TX_EXPIRED, msg: "host: tx handle expired"}
tr := transportReturning(hostErr)
ctx := context.Background()
// Every db.* verb that crosses the transport must classify TX_EXPIRED.
t.Run("query", func(t *testing.T) {
_, err := tr.query(ctx, "SELECT 1", 7, nil)
if !errors.Is(err, ErrTxExpired) {
t.Fatalf("query err = %v, want ErrTxExpired", err)
}
})
t.Run("exec", func(t *testing.T) {
_, err := tr.exec(ctx, "DELETE FROM t", 7, nil)
if !errors.Is(err, ErrTxExpired) {
t.Fatalf("exec err = %v, want ErrTxExpired", err)
}
})
t.Run("txBegin", func(t *testing.T) {
_, err := tr.txBegin(ctx)
if !errors.Is(err, ErrTxExpired) {
t.Fatalf("txBegin err = %v, want ErrTxExpired", err)
}
})
t.Run("txCommit", func(t *testing.T) {
if err := tr.txCommit(ctx, 7); !errors.Is(err, ErrTxExpired) {
t.Fatalf("txCommit err = %v, want ErrTxExpired", err)
}
})
t.Run("txRollback", func(t *testing.T) {
if err := tr.txRollback(ctx, 7); !errors.Is(err, ErrTxExpired) {
t.Fatalf("txRollback err = %v, want ErrTxExpired", err)
}
})
}
func TestTransportLeavesOtherAbiErrorsUntouched(t *testing.T) {
hostErr := &fakeHostErr{code: abiv1.AbiErrorCode_ABI_ERROR_CODE_INTERNAL, msg: "boom"}
tr := transportReturning(hostErr)
_, err := tr.query(context.Background(), "SELECT 1", 0, nil)
if errors.Is(err, ErrTxExpired) {
t.Fatalf("non-tx-expired error was misclassified as ErrTxExpired: %v", err)
}
if !errors.Is(err, error(hostErr)) {
t.Fatalf("original host error not preserved: %v", err)
}
}