fix: ignore stale desktop app markers
This commit is contained in:
parent
5d7ad6137d
commit
057624cfef
|
|
@ -9,6 +9,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
|
|
@ -116,12 +118,22 @@ func (c *commandContext) runStart(ctx context.Context, cmd *cobra.Command, opts
|
|||
|
||||
// resolveApp returns the path to a usable desktop bundle, or "" when none is
|
||||
// found (spec §6.2). Resolution order is fixed: marker path -> stat -> known
|
||||
// location scan. It never compares versions (invariant 5).
|
||||
// location scan. Known-broken desktop versions are treated as stale so `ao
|
||||
// start` can fetch a current app instead of relaunching a bad bundle forever.
|
||||
func (c *commandContext) resolveApp() string {
|
||||
if p := c.markerAppPath(); p != "" && isUsableBundle(p) {
|
||||
return p
|
||||
marker, hasMarker := c.markerAppState()
|
||||
var blockedPath string
|
||||
if hasMarker {
|
||||
if knownBrokenAppVersion(marker.Version) {
|
||||
blockedPath = filepath.Clean(marker.AppPath)
|
||||
} else if isUsableBundle(marker.AppPath) {
|
||||
return marker.AppPath
|
||||
}
|
||||
}
|
||||
for _, p := range appScanLocations() {
|
||||
if blockedPath != "" && filepath.Clean(p) == blockedPath {
|
||||
continue
|
||||
}
|
||||
if isUsableBundle(p) {
|
||||
return p
|
||||
}
|
||||
|
|
@ -133,22 +145,59 @@ func (c *commandContext) resolveApp() string {
|
|||
// tests can point the scan at a temp bundle instead of real system paths.
|
||||
var appScanLocations = knownAppLocations
|
||||
|
||||
// markerAppPath reads ~/.ao/app-state.json and returns its recorded appPath, or
|
||||
// "" if the marker is missing/unreadable. It does not stat the path; callers do.
|
||||
func (c *commandContext) markerAppPath() string {
|
||||
// markerAppState reads ~/.ao/app-state.json and returns its recorded app state,
|
||||
// or ok=false if the marker is missing/unreadable.
|
||||
func (c *commandContext) markerAppState() (appState, bool) {
|
||||
dir, err := aoStateDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
return appState{}, false
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, appStateFileName))
|
||||
if err != nil {
|
||||
return ""
|
||||
return appState{}, false
|
||||
}
|
||||
var st appState
|
||||
if err := json.Unmarshal(data, &st); err != nil {
|
||||
return ""
|
||||
return appState{}, false
|
||||
}
|
||||
return st.AppPath
|
||||
if st.AppPath == "" {
|
||||
return appState{}, false
|
||||
}
|
||||
return st, true
|
||||
}
|
||||
|
||||
// knownBrokenAppVersion reports desktop app versions that should not be reused
|
||||
// by `ao start`. Pre-0.10.0 Next.js builds could bake the build machine's
|
||||
// absolute `file://` path into the server bundle (for example
|
||||
// /private/tmp/ao-release-0.9.5), which breaks Windows launches. Treating those
|
||||
// markers as stale forces the bootstrapper to fetch a current release.
|
||||
func knownBrokenAppVersion(version string) bool {
|
||||
major, minor, _, ok := parseAppVersion(version)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return major == 0 && minor < 10
|
||||
}
|
||||
|
||||
func parseAppVersion(version string) (major, minor, patch int, ok bool) {
|
||||
v := strings.TrimSpace(version)
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
if i := strings.IndexAny(v, "-+"); i >= 0 {
|
||||
v = v[:i]
|
||||
}
|
||||
parts := strings.Split(v, ".")
|
||||
if len(parts) < 2 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
nums := [3]int{}
|
||||
for i := 0; i < len(parts) && i < len(nums); i++ {
|
||||
n, err := strconv.Atoi(parts[i])
|
||||
if err != nil {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
nums[i] = n
|
||||
}
|
||||
return nums[0], nums[1], nums[2], true
|
||||
}
|
||||
|
||||
// aoStateDir resolves the canonical ~/.ao home, honoring AO_DATA_DIR exactly as
|
||||
|
|
|
|||
|
|
@ -20,7 +20,12 @@ import (
|
|||
// configured state dir (AO_RUN_FILE's directory).
|
||||
func writeMarker(t *testing.T, cfg testConfig, appPath string) {
|
||||
t.Helper()
|
||||
st := appState{SchemaVersion: 1, AppPath: appPath, InstallSource: "npm-bootstrap"}
|
||||
writeMarkerVersion(t, cfg, appPath, "")
|
||||
}
|
||||
|
||||
func writeMarkerVersion(t *testing.T, cfg testConfig, appPath, version string) {
|
||||
t.Helper()
|
||||
st := appState{SchemaVersion: 1, AppPath: appPath, Version: version, InstallSource: "npm-bootstrap"}
|
||||
data, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -80,6 +85,20 @@ func TestResolveApp_MarkerMissThenScanHit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveApp_IgnoresKnownBrokenMarkerVersion(t *testing.T) {
|
||||
cfg := setConfigEnv(t)
|
||||
brokenBundle := makeBundle(t, appBundleName)
|
||||
replacementBundle := makeBundle(t, "replacement-"+appBundleName)
|
||||
writeMarkerVersion(t, cfg, brokenBundle, "0.9.5")
|
||||
t.Cleanup(swapScanLocations(func() []string { return []string{brokenBundle, replacementBundle} }))
|
||||
|
||||
c := &commandContext{deps: Deps{}.withDefaults()}
|
||||
got := c.resolveApp()
|
||||
if got != replacementBundle {
|
||||
t.Fatalf("resolveApp = %q, want replacement path %q", got, replacementBundle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveApp_ScanMissReturnsEmpty(t *testing.T) {
|
||||
setConfigEnv(t) // no marker written
|
||||
t.Cleanup(swapScanLocations(func() []string {
|
||||
|
|
@ -93,6 +112,25 @@ func TestResolveApp_ScanMissReturnsEmpty(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKnownBrokenAppVersion(t *testing.T) {
|
||||
tests := map[string]bool{
|
||||
"": false,
|
||||
"bogus": false,
|
||||
"0.9.5": true,
|
||||
"v0.9.5": true,
|
||||
"0.9.5-nightly.20260519": true,
|
||||
"0.10.0": false,
|
||||
"0.10.2": false,
|
||||
"0.10.3-nightly.202607041403": false,
|
||||
"1.0.0": false,
|
||||
}
|
||||
for version, want := range tests {
|
||||
if got := knownBrokenAppVersion(version); got != want {
|
||||
t.Errorf("knownBrokenAppVersion(%q) = %v, want %v", version, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetArchMapping(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
want string
|
||||
|
|
|
|||
|
|
@ -44,12 +44,29 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred
|
|||
## Install AO
|
||||
|
||||
<Tabs items={["npm", "pnpm", "yarn", "from source"]}>
|
||||
<Tab value="npm">```bash npm install -g @aoagents/ao ```</Tab>
|
||||
<Tab value="pnpm">```bash pnpm add -g @aoagents/ao ```</Tab>
|
||||
<Tab value="yarn">```bash yarn global add @aoagents/ao ```</Tab>
|
||||
<Tab value="npm">
|
||||
```bash
|
||||
npm install -g @aoagents/ao
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="pnpm">
|
||||
```bash
|
||||
pnpm add -g @aoagents/ao
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="yarn">
|
||||
```bash
|
||||
yarn global add @aoagents/ao
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="from source">
|
||||
```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
|
||||
pnpm --filter @aoagents/ao link --global ```
|
||||
```bash
|
||||
git clone https://github.com/AgentWrapper/agent-orchestrator
|
||||
cd agent-orchestrator
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm --filter @aoagents/ao link --global
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ Covers: install health, plugin resolution, notifier connectivity, stale temp fil
|
|||
## Windows-specific
|
||||
|
||||
<Accordions>
|
||||
<Accordion title="`ao start` opens a broken old dashboard with `file:///private/tmp/ao-release-0.9.5`">
|
||||
That is a stale pre-0.10 desktop build. Upgrade `@aoagents/ao`, stop AO, then run `ao start` again so the launcher can fetch the current desktop app. If the stale app still opens, delete `%USERPROFILE%\.ao\app-state.json` and `%USERPROFILE%\.ao\staging`, then retry.
|
||||
</Accordion>
|
||||
<Accordion title="`ao open` prints a URL instead of opening a tab">
|
||||
Expected on Windows. The iTerm2 helper only runs on macOS. Use the dashboard's built-in terminal (the printed URL takes you there).
|
||||
</Accordion>
|
||||
|
|
|
|||
Loading…
Reference in New Issue