feat: Added simple demo app.
This was just for the screenshot, but it can be a fun proof of concept.
This commit is contained in:
parent
b0394785c5
commit
15c73d7bfe
197
internal/cli/demo.go
Normal file
197
internal/cli/demo.go
Normal file
@ -0,0 +1,197 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"termtap.dev/internal/model"
|
||||
"termtap.dev/internal/tui"
|
||||
)
|
||||
|
||||
func runDemo() {
|
||||
events := demoEvents()
|
||||
ch := make(chan model.Event, len(events)+8)
|
||||
for _, ev := range events {
|
||||
ch <- ev
|
||||
}
|
||||
|
||||
if err := runTUIFn(ch, tui.Controls{}); err != nil {
|
||||
fatalExit(err)
|
||||
}
|
||||
}
|
||||
|
||||
func demoEvents() []model.Event {
|
||||
base := time.Now().Add(-12 * time.Second)
|
||||
request := func(method, host, path string, status int, duration time.Duration, reqBody, respBody string, reqHeaders, respHeaders http.Header) []model.Event {
|
||||
id := uuid.New()
|
||||
startedAt := base
|
||||
base = base.Add(850 * time.Millisecond)
|
||||
|
||||
requestURL := (&url.URL{Scheme: "https", Host: host, Path: path}).String()
|
||||
start := model.Event{
|
||||
Time: startedAt,
|
||||
Type: model.EventTypeRequestStarted,
|
||||
Request: model.Request{
|
||||
ID: id,
|
||||
Method: method,
|
||||
Host: host,
|
||||
URL: path,
|
||||
RawURL: requestURL,
|
||||
QueryString: "",
|
||||
Pending: true,
|
||||
StartTime: startedAt,
|
||||
RequestData: []byte(reqBody),
|
||||
RequestHeaders: reqHeaders,
|
||||
},
|
||||
}
|
||||
|
||||
finish := model.Event{
|
||||
Time: startedAt.Add(duration),
|
||||
Type: model.EventTypeRequestFinished,
|
||||
Request: model.Request{
|
||||
ID: id,
|
||||
Method: method,
|
||||
Host: host,
|
||||
URL: path,
|
||||
RawURL: requestURL,
|
||||
Status: status,
|
||||
Duration: duration,
|
||||
Pending: false,
|
||||
StartTime: startedAt,
|
||||
RequestData: []byte(reqBody),
|
||||
ResponseData: []byte(respBody),
|
||||
RequestHeaders: reqHeaders,
|
||||
ResponseHeaders: respHeaders,
|
||||
},
|
||||
}
|
||||
|
||||
return []model.Event{start, finish}
|
||||
}
|
||||
|
||||
events := []model.Event{
|
||||
{Time: base, Type: model.EventTypeSessionStarted, Body: "demo session started"},
|
||||
{Time: base.Add(100 * time.Millisecond), Type: model.EventTypeProxyStarting, Body: "proxy warming up"},
|
||||
{Time: base.Add(200 * time.Millisecond), Type: model.EventTypeProxyStarted, Body: "proxy listening on 127.0.0.1:8080"},
|
||||
{Time: base.Add(300 * time.Millisecond), Type: model.EventTypeProcessStarting, Body: "starting demo app: go run ."},
|
||||
{Time: base.Add(450 * time.Millisecond), Type: model.EventTypeProcessStarted, PID: 48213, Body: "demo app pid 48213"},
|
||||
{Time: base.Add(500 * time.Millisecond), Type: model.EventTypeProcessStdout, Body: "tap • 12 requests captured"},
|
||||
{Time: base.Add(650 * time.Millisecond), Type: model.EventTypeProcessStdout, Body: "tap • replay mode enabled"},
|
||||
{Time: base.Add(800 * time.Millisecond), Type: model.EventTypeProcessStderr, Body: "tap • upstream 429s detected"},
|
||||
}
|
||||
|
||||
events = append(events, request(
|
||||
"POST",
|
||||
"api.stripe.com",
|
||||
"/v1/payment_intents",
|
||||
200,
|
||||
183*time.Millisecond,
|
||||
`{"amount":4900,"currency":"usd","payment_method":"pm_card_visa"}`,
|
||||
`{"id":"pi_3NtDemo","status":"succeeded"}`,
|
||||
http.Header{"Authorization": []string{"Bearer sk_demo_123"}, "Content-Type": []string{"application/json"}},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"GET",
|
||||
"api.github.com",
|
||||
"/repos/lovable/tap/pulls",
|
||||
200,
|
||||
245*time.Millisecond,
|
||||
"",
|
||||
`[{"number":12,"title":"Add demo mode"}]`,
|
||||
http.Header{"Accept": []string{"application/vnd.github+json"}},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"POST",
|
||||
"api.openai.com",
|
||||
"/v1/chat/completions",
|
||||
200,
|
||||
1832*time.Millisecond,
|
||||
`{"model":"gpt-4.1-mini","stream":true}`,
|
||||
`{"id":"chatcmpl_demo","choices":[{"delta":{"content":"hello"}}]}`,
|
||||
http.Header{"Authorization": []string{"Bearer sk-openai-demo"}},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"GET",
|
||||
"api.example.com",
|
||||
"/v2/users/42/preferences",
|
||||
500,
|
||||
2105*time.Millisecond,
|
||||
"",
|
||||
`{"error":"internal_server_error"}`,
|
||||
http.Header{},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"GET",
|
||||
"hooks.slack.com",
|
||||
"/services/T0B/xxx",
|
||||
200,
|
||||
312*time.Millisecond,
|
||||
"",
|
||||
`{"ok":true}`,
|
||||
http.Header{},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"PUT",
|
||||
"api.example.com",
|
||||
"/v2/users/42/preferences",
|
||||
404,
|
||||
89*time.Millisecond,
|
||||
`{"theme":"terminal"}`,
|
||||
`{"error":"not_found"}`,
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"DELETE",
|
||||
"api.stripe.com",
|
||||
"/v1/subscriptions/sub_xyz",
|
||||
200,
|
||||
156*time.Millisecond,
|
||||
"",
|
||||
`{"status":"canceled"}`,
|
||||
http.Header{},
|
||||
http.Header{"Content-Type": {"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"PATCH",
|
||||
"api.openai.com",
|
||||
"/v1/fine_tuning/jobs",
|
||||
429,
|
||||
423*time.Millisecond,
|
||||
`{"suffix":"demo","hyperparameters":{"n_epochs":3}}`,
|
||||
`{"error":"rate_limit_exceeded"}`,
|
||||
http.Header{"Authorization": []string{"Bearer sk-openai-demo"}},
|
||||
http.Header{"Retry-After": []string{"2"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"POST",
|
||||
"api.resend.com",
|
||||
"/emails",
|
||||
422,
|
||||
67*time.Millisecond,
|
||||
`{"to":"demo@example.com"}`,
|
||||
`{"error":"unprocessable_entity"}`,
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
http.Header{"Content-Type": []string{"application/json"}},
|
||||
)...)
|
||||
events = append(events, request(
|
||||
"GET",
|
||||
"cdn.jsdelivr.net",
|
||||
"/npm/lodash@4.17.21/lodash.min.js",
|
||||
200,
|
||||
23*time.Millisecond,
|
||||
"",
|
||||
"/* minified js */",
|
||||
http.Header{},
|
||||
http.Header{"Content-Type": []string{"application/javascript"}},
|
||||
)...)
|
||||
events = append(events, model.Event{Time: base.Add(1200 * time.Millisecond), Type: model.EventTypeProcessStdout, Body: "tap • demo ready • press q to quit"})
|
||||
|
||||
return events
|
||||
}
|
||||
@ -39,6 +39,10 @@ func Run(args []string) {
|
||||
runCert()
|
||||
return
|
||||
}
|
||||
if len(args) >= 2 && args[1] == "demo" {
|
||||
runDemo()
|
||||
return
|
||||
}
|
||||
|
||||
cmd, ok := parseCommand(args)
|
||||
if !ok {
|
||||
@ -83,6 +87,7 @@ func parseCommand(args []string) (model.Command, bool) {
|
||||
func displayHelp() {
|
||||
helpText := `
|
||||
usage:
|
||||
tap demo
|
||||
tap cert
|
||||
tap run -- <command> [args...]
|
||||
`
|
||||
|
||||
@ -55,7 +55,7 @@ func TestDisplayHelpWritesToStderr(t *testing.T) {
|
||||
displayHelp()
|
||||
})
|
||||
|
||||
if !strings.Contains(stderr, "tap cert") || !strings.Contains(stderr, "tap run --") {
|
||||
if !strings.Contains(stderr, "tap demo") || !strings.Contains(stderr, "tap cert") || !strings.Contains(stderr, "tap run --") {
|
||||
t.Fatalf("stderr missing usage text: %q", stderr)
|
||||
}
|
||||
}
|
||||
@ -83,6 +83,36 @@ func TestRun_RoutesCertCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_RoutesDemoCommand(t *testing.T) {
|
||||
restore := installRunSeams(t)
|
||||
defer restore()
|
||||
|
||||
called := installFatalSpy(t)
|
||||
seen := 0
|
||||
runTUIFn = func(ch <-chan model.Event, _ tui.Controls) error {
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case ev := <-ch:
|
||||
seen++
|
||||
if seen == 1 && ev.Type != model.EventTypeSessionStarted {
|
||||
t.Fatalf("first demo event = %s, want %s", ev.Type, model.EventTypeSessionStarted)
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Run([]string{"tap", "demo"})
|
||||
if *called {
|
||||
t.Fatal("fatalExit should not be called for demo command")
|
||||
}
|
||||
if seen == 0 {
|
||||
t.Fatal("expected demo event stream to be seeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_StartSessionFailureCallsFatalExit(t *testing.T) {
|
||||
restore := installRunSeams(t)
|
||||
defer restore()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user