Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .agents/skills/dex-app-builder/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: dex-app-builder
description: Design and build this Dex AI Platform process product from business discovery through UI validation, Go backend implementation, and local verification.
---

# Dex App Builder

Read `upstream/dex-app-builder/SKILL.md` completely and follow it as the
authoritative product-development skill. Resolve every
relative reference from that upstream skill's directory.
1 change: 1 addition & 0 deletions .agents/skills/dex-app-builder/upstream
Submodule upstream added at af3c18
10 changes: 0 additions & 10 deletions .agents/skills/dex-developer/SKILL.md

This file was deleted.

1 change: 0 additions & 1 deletion .agents/skills/dex-developer/upstream
Submodule upstream deleted from 3ccc47
10 changes: 10 additions & 0 deletions .agents/skills/dex-sdk/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
name: dex-sdk
description: Implement, debug, test, and operate this application through the public Superdurable Dex Go SDK.
---

# Dex SDK

Read `../dex-app-builder/upstream/dex-sdk/SKILL.md` completely and follow it as
the authoritative Dex SDK skill. Resolve every relative reference from that
upstream skill's directory and load only its Core and Go guidance.
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule ".agents/skills/dex-developer/upstream"]
path = .agents/skills/dex-developer/upstream
url = https://github.com/superdurable/skill-dex-developer.git
[submodule ".agents/skills/dex-app-builder/upstream"]
path = .agents/skills/dex-app-builder/upstream
url = https://github.com/superdurable/dex-skills.git
6 changes: 4 additions & 2 deletions .superverse/template.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"schemaVersion": 1,
"buildProfile": "go-react-v1",
"templateVersion": "1.1.0",
"templateVersion": "1.2.0",
"minimumSandboxRuntimeContractRevision": 2,
"openapiSpec": "openapi/openapi.yaml",
"agentInstructions": "AGENTS.md",
"dexSkill": ".agents/skills/dex-developer/SKILL.md",
"dexSkill": ".agents/skills/dex-app-builder/SKILL.md",
"commands": {
"bootstrap": "make bootstrap",
"generate": "make generate",
Expand All @@ -14,8 +14,10 @@
"testUnit": "make test-unit",
"testIntegration": "make test-integration",
"testE2E": "make test-e2e",
"testMockE2E": "make test-mock-e2e",
"build": "make build",
"dev": "make dev",
"mock": "make mock",
"check": "make check"
}
}
11 changes: 9 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

This is a complete Superverse `go-react-v1` application. Read
`.superverse/template.json`, `openapi/openapi.yaml`, and the local
`dex-developer` skill before changing Dex behavior.
`dex-app-builder` skill before changing product behavior. Its pinned upstream
skill loads the sibling `dex-sdk` Core and Go guidance for backend work.

`openapi/openapi.yaml` is the only HTTP contract source. Never edit files below
`internal/api/generated` or `web/src/api/generated` by hand. Change the spec,
Expand All @@ -28,7 +29,13 @@ or delete a failing check.

Stable commands are `make bootstrap`, `make generate`, `make check-generated`,
`make check-fdg-v2`, `make test-unit`, `make test-integration`,
`make test-e2e`, `make build`, `make dev`, and `make check`.
`make test-e2e`, `make test-mock-e2e`, `make build`, `make dev`, `make mock`,
and `make check`.

`make mock` is the UI approval loop. It starts the Go in-memory mock API and
Vite HMR without Dex. Keep the mock implementation behind `cmd/mock-server`
and `/__mock__`; the production server must return 404 for mock controls.
Mock verification does not replace the real Dex integration and E2E tests.

When structure, commands, or required tooling changes, update this file,
`.superverse/template.json`, `README.md`, and contract tests together. Do not
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SHELL := /usr/bin/env bash

.PHONY: bootstrap generate check-generated check-fdg-v2 test-unit test-integration test-e2e build dev check
.PHONY: bootstrap generate check-generated check-fdg-v2 test-unit test-integration test-e2e test-mock-e2e build dev mock check
bootstrap:
go mod download
go -C tools/openapi mod download
Expand All @@ -19,16 +19,21 @@ test-integration:
./scripts/with-dex.sh go test -tags=integration ./...
test-e2e:
./scripts/with-dex.sh ./scripts/run-e2e.sh
test-mock-e2e:
./scripts/run-mock-e2e.sh
build:
npm --prefix web run build
go build -o bin/basic-process ./cmd/server
dev:
./scripts/with-dex.sh bash -c 'npm --prefix web run build && go run ./cmd/server'
mock:
./scripts/with-mock.sh
check: bootstrap check-generated check-fdg-v2
@test -z "$$(gofmt -l $$(find . -name '*.go' -not -path './.agents/*' -not -path './upstream-dex/*'))" || { gofmt -d $$(gofmt -l $$(find . -name '*.go' -not -path './.agents/*' -not -path './upstream-dex/*')); exit 1; }
go mod tidy -diff
go vet ./...
$(MAKE) test-unit
$(MAKE) test-integration
$(MAKE) test-e2e
$(MAKE) test-mock-e2e
$(MAKE) build
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ native Web v2 Action. Every Step has an FDG 2.0 group and explanation.

## Start locally

For the fastest UI interaction loop, start the in-memory mock API and Vite HMR:

```bash
make bootstrap
make mock
```

Open <http://127.0.0.1:8080>. Mock Controls can advance the process, emit a
reminder, inject the next start/refresh/approval failure, or reset all state.
The server retains state across browser refreshes and resets it on restart.
See [Local mock](docs/local-mock.md) for the complete contract.

To run the real Dex Worker and API instead:

```bash
make bootstrap
make dev
Expand Down Expand Up @@ -55,13 +69,18 @@ Never edit them manually.
make test-unit
make test-integration
make test-e2e
make test-mock-e2e
make check
```

Integration tests start a real Dex Server with `dexcli dev`. Playwright drives
the production UI and uses `dexcli flow skip-timer` to exercise the reminder
branch without waiting fifteen minutes. Every poll has a deadline.

Mock E2E runs the same frontend against the Go in-memory server without Dex.
It validates loading, failure recovery, reminders, approval, refresh restore,
completion, and reset. It does not prove durable execution behavior.

`make check-fdg-v2` validates `internal/process/flow.go` with rendering schema
2.0 and requires a diagnostic-free graph with `valid: true`. The required
preview `dexcli` source is pinned in `DEX_WEB_V2_BASELINE`; schema v1 is not an
Expand All @@ -74,14 +93,16 @@ npm, Python 3, an FDG 2.0-capable `dexcli`, Ogen's cached module dependencies,
and Chromium Headless Shell. JavaScript packages remain pinned by
`web/package-lock.json`.

## Dex skill
## Dex skills

The local skill entry delegates to the pinned public
`skill-dex-developer` submodule. Initialize it with:
The local `dex-app-builder` and `dex-sdk` entries delegate to one pinned public
`dex-skills` submodule. Initialize it with:

```bash
git submodule update --init --recursive
```

Template maintainers update the pin explicitly; generated applications never
follow the skill repository's `main` branch implicitly.
`dex-app-builder` is the product workflow entrypoint and loads the sibling
`dex-sdk` Core and Go guidance for backend implementation. Template maintainers
update the pin explicitly; generated applications never follow the skill
repository's `main` branch implicitly.
56 changes: 56 additions & 0 deletions cmd/mock-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/superdurable/dex-template-basic-process/internal/mockserver"
)

func main() {
if err := run(); err != nil {
slog.Error("mock server stopped", "error", err)
os.Exit(1)
}
}

func run() error {
handler, err := mockserver.New(mockserver.NewStore(nil))
if err != nil {
return err
}
server := &http.Server{
Addr: environment("MOCK_API_ADDRESS", "127.0.0.1:18081"),
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
}
serverResult := make(chan error, 1)
go func() { serverResult <- server.ListenAndServe() }()
slog.Info("mock API listening", "address", server.Addr)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
select {
case <-ctx.Done():
case err := <-serverResult:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("run mock HTTP server: %w", err)
}
}
shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return server.Shutdown(shutdown)
}

func environment(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
13 changes: 9 additions & 4 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ func run() error {
if err != nil {
return fmt.Errorf("create OpenAPI handler: %w", err)
}
mux := http.NewServeMux()
mux.Handle("/api/", apiHandler)
mux.Handle("/", staticHandler("web/dist"))
server := &http.Server{Addr: ":" + environment("PORT", "8080"), Handler: mux, ReadHeaderTimeout: 5 * time.Second}
server := &http.Server{Addr: ":" + environment("PORT", "8080"), Handler: applicationHandler(apiHandler), ReadHeaderTimeout: 5 * time.Second}
workerResult := runtime.StartWorker()
serverResult := make(chan error, 1)
go func() { serverResult <- server.ListenAndServe() }()
Expand All @@ -59,6 +56,14 @@ func run() error {
return server.Shutdown(shutdown)
}

func applicationHandler(apiHandler http.Handler) http.Handler {
mux := http.NewServeMux()
mux.Handle("/api/", apiHandler)
mux.Handle("/__mock__/", http.NotFoundHandler())
mux.Handle("/", staticHandler("web/dist"))
return mux
}

func staticHandler(root string) http.Handler {
files := http.FileServer(http.Dir(root))
return http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
Expand Down
19 changes: 19 additions & 0 deletions cmd/server/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestProductionHandlerDoesNotExposeMockControls(t *testing.T) {
handler := applicationHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
request := httptest.NewRequest(http.MethodGet, "/__mock__/control", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusNotFound {
t.Fatalf("mock control status = %d, want %d", response.Code, http.StatusNotFound)
}
}
54 changes: 54 additions & 0 deletions docs/local-mock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Local mock server

Use the mock server to validate UI interactions before connecting the frontend
to a real Dex Worker. It implements the application's current OpenAPI contract
with in-memory Go state and never starts Dex.

## Start

```bash
make bootstrap
make mock
```

Open <http://127.0.0.1:8080>. Vite provides hot module replacement and proxies
`/api` plus `/__mock__` to the loopback-only Go mock API.

| Variable | Default | Purpose |
| --- | --- | --- |
| `MOCK_WEB_HOST` | `0.0.0.0` | Vite bind host |
| `MOCK_WEB_PORT` | `8080` | Browser port |
| `MOCK_API_HOST` | `127.0.0.1` | Mock API bind host |
| `MOCK_API_PORT` | `18081` | Mock API port |

## Lifecycle

A new process automatically moves from `started` through `validated` to
`waiting_for_approval`. Approval moves through `approved` and `executing` to
`completed`. The short delays make loading and progress visible without
changing the real fifteen-minute Dex reminder timer.

The server keeps state across browser refreshes. Data exists only in memory and
is discarded by Reset or server restart.

## Mock Controls

The controls appear only when Vite starts with `VITE_MOCK_MODE=true`:

| Control | Behavior |
| --- | --- |
| Fail next Start | The next create request returns a one-time 503. |
| Advance | Stops automatic progression for that Flow and advances one state. |
| Emit reminder | Increments the reminder count while approval is pending. |
| Fail next Refresh | Pauses polling on a one-time 503 and exposes Retry. |
| Fail next Approval | The next approval returns a one-time 503. |
| Reset | Clears all server and browser Flow state. |

The mock-only HTTP surface is `GET /__mock__/control?flowId=...` and
`POST /__mock__/control`. The production server returns 404 for this prefix.

## Verification boundary

Run `make test-mock-e2e` for the mock interaction suite. Before handoff, always
run `make check`; only the real Dex integration and E2E suites prove durable
waits, RPC behavior, Worker replacement, Timer handling, and terminal state.
Loading
Loading