fix(ax): use typed workspace status parsing

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 16:14:08 +00:00
parent c22c63edd2
commit d005f881b7
3 changed files with 23 additions and 140 deletions

View file

@ -18,21 +18,18 @@ func (s *PrepSubsystem) registerWorkspaceCommands() {
c.Command("workspace/dispatch", core.Command{Description: "Dispatch an agent to work on a repo task", Action: s.cmdWorkspaceDispatch})
}
func (s *PrepSubsystem) cmdWorkspaceList(opts core.Options) core.Result {
fsys := s.Core().Fs()
func (s *PrepSubsystem) cmdWorkspaceList(_ core.Options) core.Result {
statusFiles := WorkspaceStatusPaths()
count := 0
for _, sf := range statusFiles {
wsName := WorkspaceName(core.PathDir(sf))
if sr := fsys.Read(sf); sr.OK {
content := sr.Value.(string)
status := extractField(content, "status")
repo := extractField(content, "repo")
agent := extractField(content, "agent")
core.Print(nil, " %-8s %-8s %-10s %s", status, agent, repo, wsName)
count++
wsDir := core.PathDir(sf)
wsName := WorkspaceName(wsDir)
st, err := ReadStatus(wsDir)
if err != nil {
continue
}
core.Print(nil, " %-8s %-8s %-10s %s", st.Status, st.Agent, st.Repo, wsName)
count++
}
if count == 0 {
core.Print(nil, " no workspaces")
@ -52,12 +49,13 @@ func (s *PrepSubsystem) cmdWorkspaceClean(opts core.Options) core.Result {
var toRemove []string
for _, sf := range statusFiles {
wsName := WorkspaceName(core.PathDir(sf))
sr := fsys.Read(sf)
if !sr.OK {
wsDir := core.PathDir(sf)
wsName := WorkspaceName(wsDir)
st, err := ReadStatus(wsDir)
if err != nil {
continue
}
status := extractField(sr.Value.(string), "status")
status := st.Status
switch filter {
case "all":
@ -130,30 +128,3 @@ func (s *PrepSubsystem) cmdWorkspaceDispatch(opts core.Options) core.Result {
}
return core.Result{OK: true}
}
// extractField does a quick JSON field extraction without full unmarshal.
func extractField(jsonStr, field string) string {
needle := core.Concat("\"", field, "\"")
idx := -1
for i := 0; i <= len(jsonStr)-len(needle); i++ {
if jsonStr[i:i+len(needle)] == needle {
idx = i + len(needle)
break
}
}
if idx < 0 {
return ""
}
for idx < len(jsonStr) && (jsonStr[idx] == ':' || jsonStr[idx] == ' ' || jsonStr[idx] == '\t') {
idx++
}
if idx >= len(jsonStr) || jsonStr[idx] != '"' {
return ""
}
idx++
end := idx
for end < len(jsonStr) && jsonStr[end] != '"' {
end++
}
return jsonStr[idx:end]
}

View file

@ -1,14 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import core "dappco.re/go/core"
func Example_extractField() {
json := `{"status":"completed","repo":"go-io"}`
core.Println(extractField(json, "status"))
core.Println(extractField(json, "repo"))
// Output:
// completed
// go-io
}

View file

@ -10,61 +10,6 @@ import (
"github.com/stretchr/testify/assert"
)
// --- extractField ---
func TestCommandsworkspace_ExtractField_Good_SimpleJSON(t *testing.T) {
json := `{"status":"running","repo":"go-io","agent":"codex"}`
assert.Equal(t, "running", extractField(json, "status"))
assert.Equal(t, "go-io", extractField(json, "repo"))
assert.Equal(t, "codex", extractField(json, "agent"))
}
func TestCommandsworkspace_ExtractField_Good_PrettyPrinted(t *testing.T) {
json := `{
"status": "completed",
"repo": "go-crypt"
}`
assert.Equal(t, "completed", extractField(json, "status"))
assert.Equal(t, "go-crypt", extractField(json, "repo"))
}
func TestCommandsworkspace_ExtractField_Good_TabSeparated(t *testing.T) {
json := `{"status": "blocked"}`
assert.Equal(t, "blocked", extractField(json, "status"))
}
func TestCommandsworkspace_ExtractField_Bad_MissingField(t *testing.T) {
json := `{"status":"running"}`
assert.Empty(t, extractField(json, "nonexistent"))
}
func TestCommandsworkspace_ExtractField_Bad_EmptyJSON(t *testing.T) {
assert.Empty(t, extractField("", "status"))
assert.Empty(t, extractField("{}", "status"))
}
func TestCommandsworkspace_ExtractField_Bad_NoValue(t *testing.T) {
// Field key exists but no quoted value after colon
json := `{"status": 42}`
assert.Empty(t, extractField(json, "status"))
}
func TestCommandsworkspace_ExtractField_Bad_TruncatedJSON(t *testing.T) {
// Field key exists but string is truncated
json := `{"status":`
assert.Empty(t, extractField(json, "status"))
}
func TestCommandsworkspace_ExtractField_Good_EmptyValue(t *testing.T) {
json := `{"status":""}`
assert.Equal(t, "", extractField(json, "status"))
}
func TestCommandsworkspace_ExtractField_Good_ValueWithSpaces(t *testing.T) {
json := `{"task":"fix the failing tests"}`
assert.Equal(t, "fix the failing tests", extractField(json, "task"))
}
// --- CmdWorkspaceList Bad/Ugly ---
func TestCommandsworkspace_CmdWorkspaceList_Bad_NoWorkspaceRootDir(t *testing.T) {
@ -75,8 +20,8 @@ func TestCommandsworkspace_CmdWorkspaceList_Bad_NoWorkspaceRootDir(t *testing.T)
c := core.New()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
r := s.cmdWorkspaceList(core.NewOptions())
@ -105,8 +50,8 @@ func TestCommandsworkspace_CmdWorkspaceList_Ugly_NonDirAndCorruptStatus(t *testi
c := core.New()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
r := s.cmdWorkspaceList(core.NewOptions())
@ -134,8 +79,8 @@ func TestCommandsworkspace_CmdWorkspaceClean_Bad_UnknownFilterLeavesEverything(t
c := core.New()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Filter "unknown" matches no switch case — nothing gets removed
@ -169,8 +114,8 @@ func TestCommandsworkspace_CmdWorkspaceClean_Ugly_MixedStatuses(t *testing.T) {
c := core.New()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// "all" filter removes completed, failed, blocked, merged, ready-for-review but NOT running/queued
@ -196,8 +141,8 @@ func TestCommandsworkspace_CmdWorkspaceDispatch_Ugly_AllFieldsSet(t *testing.T)
c := core.New()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(c, AgentOptions{}),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
r := s.cmdWorkspaceDispatch(core.NewOptions(
@ -212,22 +157,3 @@ func TestCommandsworkspace_CmdWorkspaceDispatch_Ugly_AllFieldsSet(t *testing.T)
// The test verifies the CLI correctly passes all fields through to dispatch.
assert.False(t, r.OK)
}
// --- ExtractField Ugly ---
func TestCommandsworkspace_ExtractField_Ugly_NestedJSON(t *testing.T) {
// Nested JSON — extractField only finds top-level keys (simple scan)
j := `{"outer":{"inner":"value"},"status":"ok"}`
assert.Equal(t, "ok", extractField(j, "status"))
// "inner" is inside the nested object — extractField should still find it
assert.Equal(t, "value", extractField(j, "inner"))
}
func TestCommandsworkspace_ExtractField_Ugly_EscapedQuotes(t *testing.T) {
// Value with escaped quotes — extractField stops at the first unescaped quote
j := `{"msg":"hello \"world\"","status":"done"}`
// extractField will return "hello \" because it stops at first quote after open
// The important thing is it doesn't panic
_ = extractField(j, "msg")
assert.Equal(t, "done", extractField(j, "status"))
}