Compare commits
30
Commits
stable
..
stage-9.5c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208875264f | ||
|
|
8c21400bb1 | ||
|
|
e83521a34a | ||
|
|
3f5584d062 | ||
|
|
6ce72aa1ec | ||
|
|
9ee2b0b05c | ||
|
|
99dacd7a75 | ||
|
|
5447383f2b | ||
|
|
6745d027fb | ||
|
|
e7dd01371e | ||
|
|
68802195b6 | ||
|
|
7cba364aed | ||
|
|
35e9849cae | ||
|
|
f21d455d68 | ||
|
|
d1a16d8208 | ||
|
|
2a1826f876 | ||
|
|
3af2d5d977 | ||
|
|
b61a1656ad | ||
|
|
0fbf22a090 | ||
|
|
5e763aa73c | ||
|
|
6093ca9d8b | ||
|
|
6cc0673082 | ||
|
|
ab9b003457 | ||
|
|
41d949717d | ||
|
|
69d19e501f | ||
|
|
cf68c71f05 | ||
|
|
08f0b15e1e | ||
|
|
17cb5e4291 | ||
|
|
eb9ac1e589 | ||
|
|
dae4bc19e1 |
@@ -1,4 +0,0 @@
|
||||
# Agents working on Aego
|
||||
|
||||
- Binaries go to `.cache/bin` only. Never run `go build` without `-o` in the repository root, and build test binaries only with `go test -c -o .cache/bin/<name>.test`. `make clean` empties `.cache/bin`; `make ci` fails when the root holds a binary or a `*.test` file (`tools/ci-clean-root.sh`).
|
||||
- Run the full `make ci` before every push. Between pushes, run the checks that cover the change: the tests of the changed packages, `make lint`, `make depcheck`, the docs check and the golden tests when the UI changed.
|
||||
@@ -5,4 +5,3 @@ site
|
||||
exports
|
||||
**/.aego
|
||||
*.test
|
||||
.claude
|
||||
|
||||
@@ -17,9 +17,7 @@ env:
|
||||
libasound2-dev libpulse-dev libdrm-dev libgbm-dev libudev-dev libdbus-1-dev libibus-1.0-dev
|
||||
|
||||
jobs:
|
||||
# Native runs on Windows and macOS machines; the Windows build itself is cross-compiled on Linux in ci.yml (D301).
|
||||
smoke:
|
||||
name: ${{ matrix.goos }} ${{ matrix.goarch }} smoke
|
||||
build:
|
||||
if: vars.PLATFORM_RUNNERS == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -3,7 +3,6 @@ name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
@@ -37,10 +36,6 @@ jobs:
|
||||
- run: go vet ./platform/sdl
|
||||
- run: make lint
|
||||
- run: make depcheck
|
||||
- name: packaging tools
|
||||
run: sudo apt-get install -y nsis squashfs-tools
|
||||
- name: installer dry run
|
||||
run: make nsis-check
|
||||
|
||||
build:
|
||||
strategy:
|
||||
@@ -116,42 +111,4 @@ jobs:
|
||||
- name: example project
|
||||
run: |
|
||||
/tmp/aego check examples/native-rotator
|
||||
/tmp/aego run examples/native-rotator --headless --frames 60
|
||||
|
||||
release:
|
||||
needs: [checks, build]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the key that signs release.json and the public half the binaries trust; without them no release.json is published (D362)
|
||||
AEGO_RELEASE_KEY: ${{ secrets.AEGO_RELEASE_KEY }}
|
||||
AEGO_UPDATE_KEY: ${{ vars.AEGO_UPDATE_KEY || secrets.AEGO_UPDATE_KEY }}
|
||||
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: linux packages
|
||||
run: sudo apt-get update && sudo apt-get install -y $LINUX_PKGS
|
||||
- uses: actions/cache@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: .cache/native
|
||||
key: native-linux_amd64-${{ hashFiles('third_party/sdl/version.txt', 'tools/bootstrap-native.sh') }}
|
||||
- run: bash tools/bootstrap-native.sh
|
||||
- name: windows tools
|
||||
run: sudo apt-get install -y mingw-w64 nsis zip
|
||||
- name: publish
|
||||
env:
|
||||
REQUIRE_WINDOWS: "1"
|
||||
GITEA_RESOLVE: git.akarpov.ru:443:79.142.92.250
|
||||
run: |
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/v*) sh tools/release/release.sh "$GITHUB_REF_NAME" ;;
|
||||
refs/heads/master) sh tools/release/release.sh nightly ;;
|
||||
esac
|
||||
/tmp/aego run examples/native-rotator --headless --frames 60
|
||||
@@ -18,11 +18,3 @@ tools/genicons/genicons
|
||||
/tests/golden/testdata/actual-*
|
||||
tools/websmoke/node_modules/
|
||||
tools/websmoke/golden/actual-*.png
|
||||
|
||||
# build output belongs in .cache/bin; these catch stray builds in the root
|
||||
/aego-*
|
||||
/i18n
|
||||
*.prof
|
||||
*.pprof
|
||||
/data*.txt
|
||||
*.key
|
||||
|
||||
+27
-275
File diff suppressed because one or more lines are too long
@@ -1,10 +0,0 @@
|
||||
# Aego
|
||||
|
||||
Make 2D games in Go. One editor, one project, desktop and web.
|
||||
|
||||
Build scenes in the editor, script them in a small Go-like language, extend the engine with Go when you need to, and ship the same project to Linux, Windows, macOS and the browser. Deterministic simulation, rewind, physics, animation, audio and cameras come built in.
|
||||
|
||||
- Documentation and the playground: <https://aego.akarpov.ru>
|
||||
- Installing and building from source: [engine/res/docs/install.md](engine/res/docs/install.md)
|
||||
- Contributing, check levels and rules for engine code: [docs/contributing.md](docs/contributing.md)
|
||||
- Architecture and decisions: [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
-238
File diff suppressed because it is too large
Load Diff
-234
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-109
File diff suppressed because it is too large
Load Diff
@@ -1,98 +0,0 @@
|
||||
package anim
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func clip(loop LoopMode, ts ...float32) *Clip {
|
||||
c := &Clip{Length: 1, Loop: loop}
|
||||
for _, t := range ts {
|
||||
c.Events = append(c.Events, Event{T: t, Name: "e"})
|
||||
}
|
||||
c.Fix()
|
||||
return c
|
||||
}
|
||||
|
||||
func evs(cs []Crossing) []int32 {
|
||||
out := make([]int32, len(cs))
|
||||
for i, c := range cs {
|
||||
out[i] = c.Event
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cycles(cs []Crossing) []int64 {
|
||||
out := make([]int64, len(cs))
|
||||
for i, c := range cs {
|
||||
out[i] = c.Cycle
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func expect[T comparable](t *testing.T, name string, got, want []T) {
|
||||
t.Helper()
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("%s: got %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnce(t *testing.T) {
|
||||
c := clip(LoopOnce, 0, 0.5, 1)
|
||||
expect(t, "start", evs(Crossings(c, LoopOnce, 0, 0.6, true, nil)), []int32{0, 1})
|
||||
expect(t, "end", evs(Crossings(c, LoopOnce, 0.6, 1.5, false, nil)), []int32{2})
|
||||
expect(t, "after", evs(Crossings(c, LoopOnce, 1, 2, false, nil)), []int32{})
|
||||
}
|
||||
|
||||
func TestZeroKeyEveryCycle(t *testing.T) {
|
||||
c := clip(LoopRepeat, 0)
|
||||
expect(t, "start", cycles(Crossings(c, LoopRepeat, 0, 0.5, true, nil)), []int64{0})
|
||||
expect(t, "next", cycles(Crossings(c, LoopRepeat, 0.5, 1.5, false, nil)), []int64{1})
|
||||
expect(t, "two", cycles(Crossings(c, LoopRepeat, 1.5, 3.2, false, nil)), []int64{2, 3})
|
||||
}
|
||||
|
||||
func TestLengthKeyOncePerCycle(t *testing.T) {
|
||||
c := clip(LoopRepeat, 1)
|
||||
expect(t, "start", evs(Crossings(c, LoopRepeat, 0, 0.5, true, nil)), []int32{})
|
||||
expect(t, "first", cycles(Crossings(c, LoopRepeat, 0.5, 1, false, nil)), []int64{0})
|
||||
expect(t, "second", cycles(Crossings(c, LoopRepeat, 1, 2, false, nil)), []int64{1})
|
||||
}
|
||||
|
||||
func TestSameMoment(t *testing.T) {
|
||||
c := clip(LoopOnce, 0.5, 0.5)
|
||||
expect(t, "forward", evs(Crossings(c, LoopOnce, 0, 1, false, nil)), []int32{0, 1})
|
||||
expect(t, "backward", evs(Crossings(c, LoopOnce, 1, 0, false, nil)), []int32{0, 1})
|
||||
}
|
||||
|
||||
func TestStillAndBackward(t *testing.T) {
|
||||
c := clip(LoopRepeat, 0.2, 0.8)
|
||||
expect(t, "speed 0", evs(Crossings(c, LoopRepeat, 0.3, 0.3, false, nil)), []int32{})
|
||||
got := Crossings(c, LoopRepeat, 1.5, 0.1, false, nil)
|
||||
expect(t, "backward events", evs(got), []int32{0, 1, 0})
|
||||
expect(t, "backward cycles", cycles(got), []int64{1, 0, 0})
|
||||
}
|
||||
|
||||
func TestManyCycles(t *testing.T) {
|
||||
c := clip(LoopRepeat, 0.5)
|
||||
expect(t, "three", cycles(Crossings(c, LoopRepeat, 0, 3, true, nil)), []int64{0, 1, 2})
|
||||
}
|
||||
|
||||
func TestPingPongTurns(t *testing.T) {
|
||||
c := clip(LoopPingPong, 0, 1)
|
||||
got := Crossings(c, LoopPingPong, 0, 4, true, nil)
|
||||
expect(t, "events", evs(got), []int32{0, 1, 0, 1, 0})
|
||||
expect(t, "cycles", cycles(got), []int64{0, 0, 2, 2, 4})
|
||||
mid := clip(LoopPingPong, 0.25)
|
||||
got = Crossings(mid, LoopPingPong, 0, 2, true, nil)
|
||||
if len(got) != 2 || got[0].T != 0.25 || got[1].T != 1.75 {
|
||||
t.Fatalf("mid %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossingsNoAllocs(t *testing.T) {
|
||||
c := clip(LoopRepeat, 0, 0.25, 0.5, 0.75)
|
||||
buf := make([]Crossing, 0, 64)
|
||||
if n := testing.AllocsPerRun(100, func() { buf = Crossings(c, LoopRepeat, 0.1, 3.9, false, buf) }); n != 0 {
|
||||
t.Fatalf("Crossings allocates %v", n)
|
||||
}
|
||||
}
|
||||
-332
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
||||
package anim
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/format/aetext"
|
||||
)
|
||||
|
||||
const layoutSrc = `controller version=1 {
|
||||
entry="Idle"
|
||||
param "Speed" kind=float {}
|
||||
state "Idle" {}
|
||||
state "Run" {}
|
||||
state "Jump" pos=[500, -40] {}
|
||||
state "Lost" {}
|
||||
transition "Idle" to="Run" duration=0.1 {
|
||||
cond "Speed" op=gt value=0.1 {}
|
||||
}
|
||||
transition "Run" to="Idle" duration=0.1 {
|
||||
cond "Speed" op=le value=0.1 {}
|
||||
}
|
||||
transition "any" to="Jump" {
|
||||
cond "Speed" op=lt value=0 {}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func layoutGraph(t *testing.T) *Graph {
|
||||
t.Helper()
|
||||
doc, err := aetext.Parse([]byte(layoutSrc), "l.aectrl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g, probs, err := DecodeGraph(doc)
|
||||
if err != nil || len(probs) > 0 {
|
||||
t.Fatalf("%v %+v", err, probs)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func TestLayoutPlacesEveryStateApart(t *testing.T) {
|
||||
g := layoutGraph(t)
|
||||
g.Layout()
|
||||
seen := map[[2]float32]string{}
|
||||
for _, s := range g.States {
|
||||
if !s.HasPos {
|
||||
t.Fatalf("%s has no position", s.Name)
|
||||
}
|
||||
if o, dup := seen[s.Pos]; dup {
|
||||
t.Fatalf("%s and %s share %v", s.Name, o, s.Pos)
|
||||
}
|
||||
seen[s.Pos] = s.Name
|
||||
}
|
||||
idle, run, jump := g.States[0].Pos, g.States[1].Pos, g.States[2].Pos
|
||||
if idle[0] != 0 || run[0] != LayoutColumn {
|
||||
t.Fatalf("entry %v, next %v: the entry is the first column", idle, run)
|
||||
}
|
||||
if jump != [2]float32{500, -40} {
|
||||
t.Fatalf("a written position moved to %v", jump)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPosRoundTrips(t *testing.T) {
|
||||
g := layoutGraph(t)
|
||||
g.Layout()
|
||||
text := aetext.Write(EncodeGraph(g))
|
||||
doc, err := aetext.Parse(text, "l.aectrl")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
back, _, err := DecodeGraph(doc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bin, err := UnmarshalGraph(MarshalGraph(back))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range g.States {
|
||||
if back.States[i].Pos != g.States[i].Pos || bin.States[i].Pos != g.States[i].Pos {
|
||||
t.Fatalf("%s: text %v binary %v, want %v", g.States[i].Name, back.States[i].Pos, bin.States[i].Pos, g.States[i].Pos)
|
||||
}
|
||||
}
|
||||
if _, err := UnmarshalGraph(MarshalGraph(back)[:len(MarshalGraph(back))-4]); err == nil {
|
||||
t.Fatal("a truncated state was accepted")
|
||||
}
|
||||
}
|
||||
-185
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
package anim
|
||||
|
||||
// RootDelta is the change of a root track between two clip times: a looping clip adds its end minus its start for every cycle crossed, a ping-pong clip only moves back and forth.
|
||||
func RootDelta(c *Clip, s *Sampler, track int, loop LoopMode, t0, t1 float64) [2]float32 {
|
||||
if !(c.Length > 0) || t0 == t1 || track < 0 || track >= len(c.Tracks) {
|
||||
return [2]float32{}
|
||||
}
|
||||
u0, k0 := c.Local(loop, t0)
|
||||
u1, k1 := c.Local(loop, t1)
|
||||
a := s.Track(c, track, u0).L
|
||||
b := s.Track(c, track, u1).L
|
||||
d := [2]float32{b[0] - a[0], b[1] - a[1]}
|
||||
if loop == LoopRepeat && k1 != k0 {
|
||||
start := s.Track(c, track, 0).L
|
||||
end := s.Track(c, track, c.Length).L
|
||||
n := float32(k1 - k0)
|
||||
d[0] += n * (end[0] - start[0])
|
||||
d[1] += n * (end[1] - start[1])
|
||||
}
|
||||
return d
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package anim
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/core/curve"
|
||||
"aego/schema"
|
||||
)
|
||||
|
||||
func walkClip(loop LoopMode) *Clip {
|
||||
c := &Clip{Length: 1, Loop: loop, Tracks: []Track{{
|
||||
Property: RootProp, Kind: schema.KindVec2, Root: true,
|
||||
Keys: []Key{{T: 0, V: Lanes([4]float32{0, 0}), Interp: curve.InterpLinear}, {T: 1, V: Lanes([4]float32{1, 0.5})}},
|
||||
}}}
|
||||
c.Fix()
|
||||
return c
|
||||
}
|
||||
|
||||
func TestRootDelta(t *testing.T) {
|
||||
var s Sampler
|
||||
loop := walkClip(LoopRepeat)
|
||||
for _, tc := range []struct {
|
||||
t0, t1 float64
|
||||
want [2]float32
|
||||
}{
|
||||
{0.25, 0.75, [2]float32{0.5, 0.25}},
|
||||
{0.5, 1.5, [2]float32{1, 0.5}},
|
||||
{0.25, 2.25, [2]float32{2, 1}},
|
||||
{1.25, 0.25, [2]float32{-1, -0.5}},
|
||||
{0.75, 1, [2]float32{0.25, 0.125}},
|
||||
} {
|
||||
d := RootDelta(loop, &s, 0, LoopRepeat, tc.t0, tc.t1)
|
||||
if !near(d[0], tc.want[0]) || !near(d[1], tc.want[1]) {
|
||||
t.Fatalf("loop %v..%v: %v, want %v", tc.t0, tc.t1, d, tc.want)
|
||||
}
|
||||
}
|
||||
ping := walkClip(LoopPingPong)
|
||||
if d := RootDelta(ping, &s, 0, LoopPingPong, 0.5, 1.5); !near(d[0], 0) || !near(d[1], 0) {
|
||||
t.Fatalf("ping-pong turns back: %v", d)
|
||||
}
|
||||
once := walkClip(LoopOnce)
|
||||
if d := RootDelta(once, &s, 0, LoopOnce, 0.75, 3); !near(d[0], 0.25) {
|
||||
t.Fatalf("once stops at the end: %v", d)
|
||||
}
|
||||
if d := RootDelta(loop, &s, 0, LoopRepeat, 0.5, 0.5); d != ([2]float32{}) {
|
||||
t.Fatalf("no time, no motion: %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCheck(t *testing.T) {
|
||||
c := walkClip(LoopRepeat)
|
||||
if ps := c.Check(nil); len(ps) != 0 {
|
||||
t.Fatalf("a good root track reports %+v", ps)
|
||||
}
|
||||
c.Tracks[0].Target = "Body"
|
||||
if ps := c.Check(nil); len(ps) != 1 || ps[0].Kind != ProblemRoot {
|
||||
t.Fatalf("a root track on a child: %+v", ps)
|
||||
}
|
||||
}
|
||||
-146
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,73 +0,0 @@
|
||||
package anim
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/format/aetext"
|
||||
)
|
||||
|
||||
// shape lists the attributes, fields and child blocks a block of a clip or controller may hold.
|
||||
type shape struct {
|
||||
attrs []string
|
||||
fields []string
|
||||
blocks []string
|
||||
}
|
||||
|
||||
var (
|
||||
clipShape = shape{attrs: []string{"version", "length", "fps", "loop"}, blocks: []string{"track", "frames", "event"}}
|
||||
trackShape = shape{attrs: []string{"prop", "kind", "numeric", "root", "authority", "onEnd"}, blocks: []string{"key"}}
|
||||
keyShape = shape{attrs: []string{"t", "v", "interp", "tangent", "in", "out"}}
|
||||
framesShape = shape{attrs: []string{"start", "fps", "prop", "flipProp", "authority"}, fields: []string{"sprites", "durations", "flipX"}}
|
||||
eventShape = shape{attrs: []string{"t", "name", "payload", "mode"}}
|
||||
ctrlShape = shape{attrs: []string{"version", "ease", "entryPos", "anyPos"}, fields: []string{"entry"}, blocks: []string{"param", "state", "transition"}}
|
||||
paramShape = shape{attrs: []string{"kind", "default"}}
|
||||
stateShape = shape{attrs: []string{"clip", "speed", "loop", "pos", "events"}}
|
||||
transShape = shape{attrs: []string{"to", "duration", "exitTime", "interrupt"}, blocks: []string{"cond"}}
|
||||
condShape = shape{attrs: []string{"op", "value"}}
|
||||
)
|
||||
|
||||
// opening is how the block's first line reads with the attribute moved into it.
|
||||
func opening(n *aetext.Node, f aetext.Field) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(n.Head)
|
||||
if n.Label != "" || n.Head == "frames" || n.Head == "track" {
|
||||
b.WriteString(" " + strconv.Quote(n.Label))
|
||||
}
|
||||
b.WriteString(" " + f.Name + "=" + aetext.FormatValue(f.Value) + " {")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// strict refuses names a block does not know, so a misspelt or misplaced setting is never dropped silently.
|
||||
func strict(n *aetext.Node, s shape) error {
|
||||
for _, f := range n.Attrs {
|
||||
switch {
|
||||
case slices.Contains(s.attrs, f.Name):
|
||||
case slices.Contains(s.fields, f.Name):
|
||||
return textErr(n, f.Name+" is a field of "+n.Head+": write "+f.Name+"="+aetext.FormatValue(f.Value)+" inside the block")
|
||||
default:
|
||||
return textErr(n, "unknown attribute "+f.Name+" of "+n.Head+"; it takes "+strings.Join(s.attrs, ", "))
|
||||
}
|
||||
}
|
||||
for _, f := range n.Fields {
|
||||
switch {
|
||||
case slices.Contains(s.fields, f.Name):
|
||||
case slices.Contains(s.attrs, f.Name):
|
||||
return textErr(n, f.Name+" is an attribute of "+n.Head+": write `"+opening(n, f)+"`")
|
||||
case len(s.fields) == 0:
|
||||
return textErr(n, "unknown field "+f.Name+": "+n.Head+" takes no fields")
|
||||
default:
|
||||
return textErr(n, "unknown field "+f.Name+" of "+n.Head+"; it takes "+strings.Join(s.fields, ", "))
|
||||
}
|
||||
}
|
||||
for _, b := range n.Blocks {
|
||||
if !slices.Contains(s.blocks, b.Head) {
|
||||
if len(s.blocks) == 0 {
|
||||
return textErr(b, n.Head+" holds no blocks, found "+b.Head)
|
||||
}
|
||||
return textErr(b, "unknown block "+b.Head+" in "+n.Head+"; it holds "+strings.Join(s.blocks, ", "))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package anim
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aego/format/aetext"
|
||||
)
|
||||
|
||||
func decodeText(t *testing.T, src string, ctrl bool) error {
|
||||
t.Helper()
|
||||
n, err := aetext.Parse([]byte(src), "t")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ctrl {
|
||||
_, _, err = DecodeGraph(n)
|
||||
} else {
|
||||
_, err = Decode(n)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Every block of a clip refuses names it does not know instead of dropping them.
|
||||
func TestClipRefusesUnknownNames(t *testing.T) {
|
||||
cases := []struct{ name, src, want string }{
|
||||
{"root", `anim version=1 length=1 speed=2 {}`, "unknown attribute speed of anim"},
|
||||
{"root block", "anim version=1 length=1 {\n curve \"\" {}\n}\n", "unknown block curve"},
|
||||
{"track", "anim version=1 length=1 {\n track \"\" prop=\"SpriteRenderer.size\" kind=vec2 loop=true {}\n}\n", "unknown attribute loop of track"},
|
||||
{"key", "anim version=1 length=1 {\n track \"\" prop=\"SpriteRenderer.size\" kind=vec2 {\n key t=0 v=[1, 1] ease=linear {}\n }\n}\n", "unknown attribute ease of key"},
|
||||
{"frames", "anim version=1 length=1 {\n frames \"Slash\" {\n sprites=[asset(\"ae900000000000000000000000000b01\")]\n start=0.08\n }\n}\n", "start is an attribute of frames: write `frames \"Slash\" start=0.08 {`"},
|
||||
{"frames field", "anim version=1 length=1 {\n frames \"\" {\n sprites=[asset(\"ae900000000000000000000000000b01\")]\n duration=[0.1]\n }\n}\n", "unknown field duration of frames"},
|
||||
{"event", "anim version=1 length=1 {\n event t=0.1 name=\"Hit\" when=1 {}\n}\n", "unknown attribute when of event"},
|
||||
{"event field", "anim version=1 length=1 {\n event t=0.1 name=\"Hit\" {\n payload=1\n }\n}\n", "payload is an attribute of event"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
err := decodeText(t, c.src, false)
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: got %v, want %q", c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every block of a controller refuses names it does not know instead of dropping them.
|
||||
func TestControllerRefusesUnknownNames(t *testing.T) {
|
||||
cases := []struct{ name, src, want string }{
|
||||
{"root", "controller version=1 start=\"Idle\" {\n state \"Idle\" {}\n}\n", "unknown attribute start of controller"},
|
||||
{"root field", "controller version=1 {\n ease=linear\n state \"Idle\" {}\n}\n", "ease is an attribute of controller"},
|
||||
{"param", "controller version=1 {\n param \"Speed\" kind=float min=0 {}\n}\n", "unknown attribute min of param"},
|
||||
{"state", "controller version=1 {\n state \"Idle\" clipp=asset(\"a0a10000000000000000000000000005\") {}\n}\n", "unknown attribute clipp of state"},
|
||||
{"transition", "controller version=1 {\n state \"A\" {}\n state \"B\" {}\n transition \"A\" to=\"B\" exit=1 {}\n}\n", "unknown attribute exit of transition"},
|
||||
{"cond", "controller version=1 {\n param \"Speed\" kind=float {}\n state \"A\" {}\n state \"B\" {}\n transition \"A\" to=\"B\" {\n cond \"Speed\" op=gt val=1 {}\n }\n}\n", "unknown attribute val of cond"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
err := decodeText(t, c.src, true)
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: got %v, want %q", c.name, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
-521
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
||||
package anim
|
||||
|
||||
import (
|
||||
"aego/core/ids"
|
||||
"aego/format/aetext"
|
||||
"aego/schema"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
L [4]float32
|
||||
I int64
|
||||
A ids.AssetID
|
||||
}
|
||||
|
||||
func Float(f float32) Value { return Value{L: [4]float32{f}} }
|
||||
func Lanes(l [4]float32) Value { return Value{L: l} }
|
||||
func Int(i int64) Value { return Value{I: i} }
|
||||
func Asset(id ids.AssetID) Value { return Value{A: id} }
|
||||
func (v Value) Truth() bool { return v.I != 0 }
|
||||
|
||||
func Bool(b bool) Value {
|
||||
if b {
|
||||
return Value{I: 1}
|
||||
}
|
||||
return Value{}
|
||||
}
|
||||
|
||||
type class uint8
|
||||
|
||||
const (
|
||||
classNone class = iota
|
||||
classLanes
|
||||
classInt
|
||||
classDiscrete
|
||||
classQuat
|
||||
classAsset
|
||||
)
|
||||
|
||||
func classOf(k schema.Kind) class {
|
||||
switch k {
|
||||
case schema.KindFloat32, schema.KindFloat64, schema.KindVec2, schema.KindVec3, schema.KindVec4, schema.KindColor, schema.KindRect:
|
||||
return classLanes
|
||||
case schema.KindInt32, schema.KindInt64:
|
||||
return classInt
|
||||
case schema.KindBool, schema.KindEnum:
|
||||
return classDiscrete
|
||||
case schema.KindQuat:
|
||||
return classQuat
|
||||
case schema.KindAssetRef:
|
||||
return classAsset
|
||||
}
|
||||
return classNone
|
||||
}
|
||||
|
||||
func lanesOf(k schema.Kind) int {
|
||||
switch k {
|
||||
case schema.KindFloat32, schema.KindFloat64:
|
||||
return 1
|
||||
case schema.KindVec2:
|
||||
return 2
|
||||
case schema.KindVec3:
|
||||
return 3
|
||||
}
|
||||
return 4
|
||||
}
|
||||
|
||||
func Supported(k schema.Kind) bool {
|
||||
return classOf(k) != classNone
|
||||
}
|
||||
|
||||
func Interpolates(k schema.Kind) bool {
|
||||
c := classOf(k)
|
||||
return c == classLanes || c == classQuat
|
||||
}
|
||||
|
||||
// LaneCount is the number of float lanes of a lane kind, 0 for discrete kinds.
|
||||
func LaneCount(k schema.Kind) int {
|
||||
if classOf(k) != classLanes && classOf(k) != classQuat {
|
||||
return 0
|
||||
}
|
||||
return lanesOf(k)
|
||||
}
|
||||
|
||||
// ValueText writes v the way a key stores it in .aeanim.
|
||||
func ValueText(kind schema.Kind, v Value) aetext.Value {
|
||||
return writeValue(kind, v)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,61 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func tree(depth, fan int) NodeView {
|
||||
v := NodeView{Name: "n"}
|
||||
if depth > 0 {
|
||||
for i := 0; i < fan; i++ {
|
||||
v.Children = append(v.Children, tree(depth-1, fan))
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestCapTreeKeepsTheFirstNodes(t *testing.T) {
|
||||
v := tree(4, 4)
|
||||
all := nodeCount(&v)
|
||||
left := capTree(&v, 50, 0)
|
||||
if kept := nodeCount(&v); kept != 50 || kept+left != all {
|
||||
t.Fatalf("kept %d and left %d of %d", kept, left, all)
|
||||
}
|
||||
more := 0
|
||||
var walk func(n *NodeView)
|
||||
walk = func(n *NodeView) {
|
||||
more += n.More
|
||||
for i := range n.Children {
|
||||
walk(&n.Children[i])
|
||||
}
|
||||
}
|
||||
walk(&v)
|
||||
if more != left {
|
||||
t.Fatalf("the more counts add up to %d, %d were left out", more, left)
|
||||
}
|
||||
v = tree(4, 2)
|
||||
if left := capTree(&v, 0, 2); nodeCount(&v) != 7 || left != 31-7 {
|
||||
t.Fatalf("depth 2 kept %d nodes", nodeCount(&v))
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptWindow(t *testing.T) {
|
||||
var b strings.Builder
|
||||
for i := 1; i <= 1000; i++ {
|
||||
b.WriteString("line " + strconv.Itoa(i) + "\n")
|
||||
}
|
||||
text := b.String()
|
||||
if got, note := window(text, 0, 0, 2000); got != text || note != "" {
|
||||
t.Fatal("a short script is not read whole")
|
||||
}
|
||||
got, note := window(text, 0, 0, 400)
|
||||
if !strings.HasPrefix(got, "line 1\n") || !strings.HasSuffix(got, "line 400\n") || !strings.Contains(note, "1-400 of 1001") {
|
||||
t.Fatalf("the first window is wrong: %q", note)
|
||||
}
|
||||
got, _ = window(text, 600, 620, 400)
|
||||
if !strings.HasPrefix(got, "line 600\n") || !strings.HasSuffix(got, "line 620\n") {
|
||||
t.Fatalf("the asked range is wrong: %q", got[:20])
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,85 +0,0 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustFind(t *testing.T, name string) *Command {
|
||||
t.Helper()
|
||||
c, ok := Find(name)
|
||||
if !ok {
|
||||
t.Fatalf("no command %s", name)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
set := mustFind(t, "scene.set")
|
||||
a, f, err := parseArgs(set, []string{"s.aescene", "--json", "Player", "--dry-run", "Body2D.mass", "-3"})
|
||||
if err != nil || !f.json || a.Str("value") != "-3" || a.Str("node") != "Player" || !a.Bool("dry-run") {
|
||||
t.Fatalf("flags after positionals: %v %+v %v", a, f, err)
|
||||
}
|
||||
add := mustFind(t, "scene.add-node")
|
||||
for _, toks := range [][]string{{"s", "N", "--index=1"}, {"s", "N", "--index", "1"}} {
|
||||
a, _, err := parseArgs(add, toks)
|
||||
if n, _ := a.Int("index", -1); err != nil || n != 1 {
|
||||
t.Fatalf("%v: %v %v", toks, a, err)
|
||||
}
|
||||
}
|
||||
a, _, err = parseArgs(set, []string{"s", "--", "Player", "Body2D.mass", "--odd"})
|
||||
if err != nil || a.Str("value") != "--odd" {
|
||||
t.Fatalf("after --: %v %v", a, err)
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "--nope"}); err == nil {
|
||||
t.Fatal("an unknown flag was accepted")
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "a", "b", "c", "d"}); err == nil {
|
||||
t.Fatal("an extra positional was accepted")
|
||||
}
|
||||
var out, errw bytes.Buffer
|
||||
if code := Main(nil, []string{"scene", "set", "s", "--nope"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("unknown flag exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "set", "a", "b", "c", "d", "e"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("extra positional exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "help"}, &out, &errw, Options{}); code != 0 {
|
||||
t.Fatalf("help exit %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemasAndToolNames(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, c := range Commands() {
|
||||
if seen[c.Tool()] {
|
||||
t.Fatalf("tool name %s is used twice", c.Tool())
|
||||
}
|
||||
seen[c.Tool()] = true
|
||||
b, err := json.Marshal(c.Schema())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sch struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]map[string]any `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &sch); err != nil || sch.Type != "object" {
|
||||
t.Fatalf("%s: %s", c.Name, b)
|
||||
}
|
||||
var want []string
|
||||
for _, x := range c.AllArgs() {
|
||||
if x.Required {
|
||||
want = append(want, x.Name)
|
||||
}
|
||||
if _, ok := sch.Properties[x.Name]; !ok {
|
||||
t.Fatalf("%s: no property %s", c.Name, x.Name)
|
||||
}
|
||||
}
|
||||
if len(want) != len(sch.Required) {
|
||||
t.Fatalf("%s: required %v, want %v", c.Name, sch.Required, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,94 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxBody = 8 << 20
|
||||
|
||||
// HTTP serves MCP as JSON-RPC over POST on loopback, for an agent the editor runs (D341).
|
||||
type HTTP struct {
|
||||
URL string
|
||||
Token string
|
||||
srv *http.Server
|
||||
s *Server
|
||||
}
|
||||
|
||||
// ListenHTTP serves on 127.0.0.1 at a free port with a fresh bearer token.
|
||||
func ListenHTTP(opt Options) (*HTTP, error) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var key [32]byte
|
||||
if _, err := rand.Read(key[:]); err != nil {
|
||||
ln.Close()
|
||||
return nil, err
|
||||
}
|
||||
h := &HTTP{URL: "http://" + ln.Addr().String() + "/mcp", Token: hex.EncodeToString(key[:]), s: newServer(opt)}
|
||||
h.s.ready.Store(true)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/mcp", h.serve)
|
||||
h.srv = &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second}
|
||||
go h.srv.Serve(ln)
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *HTTP) authorized(r *http.Request) bool {
|
||||
got := r.Header.Get("Authorization")
|
||||
return subtle.ConstantTimeCompare([]byte(got), []byte("Bearer "+h.Token)) == 1
|
||||
}
|
||||
|
||||
// local refuses a request whose Host is not loopback, so a web page cannot reach the server by DNS rebinding.
|
||||
func local(r *http.Request) bool {
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
return host == "127.0.0.1" || host == "localhost"
|
||||
}
|
||||
|
||||
func (h *HTTP) serve(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case !local(r):
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
case !h.authorized(r):
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
case r.Method != http.MethodPost:
|
||||
w.Header().Set("Allow", http.MethodPost)
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(r.Body, maxBody))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rep, ok := h.s.respond(raw)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.Encode(rep)
|
||||
}
|
||||
|
||||
func (h *HTTP) Close() error {
|
||||
return h.srv.Close()
|
||||
}
|
||||
|
||||
// Definitions is what the model reads about this server before the first message.
|
||||
func (h *HTTP) Definitions() string {
|
||||
return h.s.Definitions()
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"aego/app/agent"
|
||||
)
|
||||
|
||||
// Relay passes an outside agent's MCP session to the open editor, so its changes become the editor's Undo steps, named after the client.
|
||||
func Relay(in io.Reader, out io.Writer, addr, token string) error {
|
||||
client := "agent"
|
||||
sc := bufio.NewScanner(in)
|
||||
sc.Buffer(make([]byte, 0, 1<<20), maxBody)
|
||||
for sc.Scan() {
|
||||
line := sc.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
var msg map[string]any
|
||||
if err := json.Unmarshal(line, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
params, _ := msg["params"].(map[string]any)
|
||||
switch msg["method"] {
|
||||
case "initialize":
|
||||
if info, ok := params["clientInfo"].(map[string]any); ok {
|
||||
if name, ok := info["name"].(string); ok && name != "" {
|
||||
client = name
|
||||
}
|
||||
}
|
||||
case "tools/call":
|
||||
if params != nil {
|
||||
args, _ := params["arguments"].(map[string]any)
|
||||
if args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
args[agent.ViaArg] = "via agent: " + client
|
||||
params["arguments"] = args
|
||||
}
|
||||
}
|
||||
body, _ := json.Marshal(msg)
|
||||
rep, err := agent.ProxyPost(addr, token, body)
|
||||
if err != nil {
|
||||
id, ok := msg["id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rep, _ = json.Marshal(map[string]any{"jsonrpc": "2.0", "id": id, "error": map[string]any{"code": -32000, "message": "the editor closed; start the MCP server again: " + err.Error()}})
|
||||
}
|
||||
if len(rep) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := out.Write(append(rep, '\n')); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return sc.Err()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"aego/app/agent"
|
||||
"aego/project"
|
||||
)
|
||||
|
||||
// Source runs the tool catalog: the CLI opens a session per call, the editor runs on its own documents (D341).
|
||||
type Source interface {
|
||||
Run(name string, a agent.Args) (*agent.Output, error)
|
||||
Paths() ([]string, error)
|
||||
Project() *project.Project
|
||||
Rel(p string) (string, error)
|
||||
}
|
||||
|
||||
// Extra is a resource a Source adds to the list, such as the editor's selection.
|
||||
type Extra struct {
|
||||
URI string
|
||||
Name string
|
||||
Description string
|
||||
MimeType string
|
||||
Read func() (string, error)
|
||||
}
|
||||
|
||||
// Tool is a tool the host adds next to the agent commands, such as the editor's ask_user.
|
||||
type Tool struct {
|
||||
Name string
|
||||
Description string
|
||||
Schema map[string]any
|
||||
ReadOnly bool
|
||||
// Run answers the call; it may block while the user decides.
|
||||
Run func(args map[string]any) (text string, failed bool)
|
||||
}
|
||||
|
||||
type kitSource struct {
|
||||
kit *agent.Kit
|
||||
}
|
||||
|
||||
func (k kitSource) Run(name string, a agent.Args) (*agent.Output, error) {
|
||||
sess, err := k.kit.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sess.Close()
|
||||
return agent.Run(sess, name, a)
|
||||
}
|
||||
|
||||
func (k kitSource) Paths() ([]string, error) {
|
||||
sess, err := k.kit.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sess.Close()
|
||||
var out []string
|
||||
for _, a := range sess.Pipeline.Assets() {
|
||||
out = append(out, a.Path)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (k kitSource) Project() *project.Project {
|
||||
return k.kit.Project
|
||||
}
|
||||
|
||||
func (k kitSource) Rel(p string) (string, error) {
|
||||
return k.kit.Rel(p)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
// Package backend runs a model for the Agent panel behind one interface: the Claude Code CLI, or a scripted fake for tests (D341).
|
||||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type EventKind uint8
|
||||
|
||||
const (
|
||||
EvText EventKind = iota
|
||||
EvToolStart
|
||||
EvToolResult
|
||||
EvTurnEnd
|
||||
EvError
|
||||
// EvUsage reports Tokens, the context the model read for its last answer, and Model.
|
||||
EvUsage
|
||||
// EvCompact reports that the CLI compacted the history; Tokens is the context before it.
|
||||
EvCompact
|
||||
)
|
||||
|
||||
// ToolPrefix is how Claude Code names the tools of the aego MCP server.
|
||||
const ToolPrefix = "mcp__aego__"
|
||||
|
||||
type Event struct {
|
||||
Kind EventKind
|
||||
Text string
|
||||
Tool string
|
||||
CallID string
|
||||
Args json.RawMessage
|
||||
Result string
|
||||
Failed bool
|
||||
Err error
|
||||
// Cost in US dollars and Millis of the answer, on EvTurnEnd.
|
||||
Cost float64
|
||||
Millis int
|
||||
// Tokens on EvUsage and EvCompact; Window is the model's context size, on EvTurnEnd when the CLI reports it.
|
||||
Tokens int
|
||||
Window int
|
||||
Model string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Dir string
|
||||
Command string
|
||||
MCPURL string
|
||||
Token string
|
||||
System string
|
||||
// Record gets every line the CLI writes, for recording sessions the tests replay.
|
||||
Record io.Writer
|
||||
}
|
||||
|
||||
type Conversation interface {
|
||||
Send(text string) error
|
||||
Events() <-chan Event
|
||||
Cancel()
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
Name() string
|
||||
Available(cfg Config) (string, error)
|
||||
Start(cfg Config) (Conversation, error)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user