diff --git a/cmd/internal-tools/bundle-check/main.go b/cmd/internal-tools/bundle-check/main.go new file mode 100644 index 0000000..5566ea8 --- /dev/null +++ b/cmd/internal-tools/bundle-check/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/priyanshujain/uatu/internal/bundler" +) + +func main() { + repoRoot, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "getwd: %v\n", err) + os.Exit(1) + } + specApiPath := filepath.Join(repoRoot, "pkg/spec-api/src/index.ts") + + result, err := bundler.Bundle(bundler.Options{ + EntryFile: "examples/specs/merchant-ledger.ts", + Defines: map[string]string{ + "UATU_TEST_PHONE": "+910000000000", + "UATU_TEST_OTP": "000000", + }, + Aliases: map[string]string{ + "@uatu/spec": specApiPath, + }, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "bundle: %v\n", err) + os.Exit(1) + } + fmt.Printf("bundled: %d bytes, sha256=%s\n", len(result.JavaScript), result.SHA256) +} diff --git a/examples/specs/merchant-ledger.ts b/examples/specs/merchant-ledger.ts new file mode 100644 index 0000000..6276c87 --- /dev/null +++ b/examples/specs/merchant-ledger.ts @@ -0,0 +1,96 @@ +import { + extract, + always, + actions, + weighted, + Tap, + InputText, + taps, + swipes, +} from "@uatu/spec"; +import type { State } from "@uatu/spec"; + +// ── Snapshot extractors ──────────────────────────────────────── +const screen = extract((state) => (state.snapshots.screen as string) ?? ""); +const balance = extract( + (state) => (state.snapshots["ledger.balance"] as number) ?? 0, +); +const totalGiven = extract( + (state) => (state.snapshots["ledger.totalGiven"] as number) ?? 0, +); +const totalReceived = extract( + (state) => (state.snapshots["ledger.totalReceived"] as number) ?? 0, +); + +// ── Screen detection via accessibility tree ──────────────────── +const onLoginPhone = extract((state) => + Boolean(state.ax.find("id:login_phone_field")), +); +const onLoginOtp = extract((state) => + Boolean(state.ax.find("id:login_otp_field")), +); +const onHome = extract( + (state) => + state.ax.findAll("id:home_customer_row").length > 0 || + state.ax.findAll("id:home_supplier_row").length > 0, +); +const onLedger = extract( + () => screen.current === "customer_ledger" || screen.current === "supplier_ledger", +); + +// ── Properties ───────────────────────────────────────────────── +export const properties = { + // The displayed balance is server-fed; the totals are local-DB-fed. + // Any divergence — stale cache, partial sync, optimistic-update glitch, + // mishandled deleted txn — is a violation. + ledgerBalanceMatchesTxns: always( + () => + !onLedger.current || + balance.current === totalGiven.current - totalReceived.current, + ), +}; + +// ── Action generators (gated by screen state) ────────────────── +const enterPhone = actions(() => + onLoginPhone.current + ? [ + InputText({ + into: "id:login_phone_field", + text: process.env.UATU_TEST_PHONE ?? "", + }), + Tap({ on: "id:login_continue" }), + ] + : [], +); + +const enterOtp = actions(() => + onLoginOtp.current + ? [ + InputText({ + into: "id:login_otp_field", + text: process.env.UATU_TEST_OTP ?? "", + }), + ] + : [], +); + +const openCustomerOrSupplier = actions((): ReturnType[] => { + if (!onHome.current) return []; + // The verifier resolves the selector; we just hand back a list of + // candidate row taps weighted equally by the runtime's pickFromResult. + return [ + Tap({ on: "id:home_customer_row" }), + Tap({ on: "id:home_supplier_row" }), + ]; +}); + +export const actionsRoot = weighted( + [100, enterPhone], + [100, enterOtp], + [80, openCustomerOrSupplier], + [10, taps], + [2, swipes], +); + +// Verifier looks for `globalThis.actions`; re-export the weighted root. +(globalThis as { actions?: unknown }).actions = actionsRoot;