- status: read-only report of deployed/repository revision, pending changes, skipped items (with reasons), unrecognized local items, binary version; --json machine-readable; exit 1 when local modifications block repository updates (headless drift checks) - sync/status share a read-only prepare() front half - notify package: windows toast via PowerShell script (go-toast mechanism, zero deps, build-tag gated), noop on linux/macos; cross-compile verified - toast fires exactly once per sync with warnings, never for routine syncs; notification failures are logged and never fail the sync (fail-open)
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
package e2e
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func statusRun(t *testing.T, env *syncEnv, jsonFlag bool) (string, error) {
|
|
t.Helper()
|
|
args := []string{"status", "--config", env.config}
|
|
if jsonFlag {
|
|
args = append(args, "--json")
|
|
}
|
|
cmd := agentLibCmd(args...)
|
|
cmd.Env = append(os.Environ(),
|
|
"HOME="+env.home, "USERPROFILE="+env.home,
|
|
"AGENT_LIB_STATE_DIR="+env.stateDir,
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
func TestStatusInSync(t *testing.T) {
|
|
h := newHarness(t)
|
|
env := newSyncEnv(t, h)
|
|
buildWorkRepo(t, h, env.workRepo)
|
|
env.writeConfig(t, env.workRepo, "")
|
|
syncOnce(t, env)
|
|
|
|
out, err := statusRun(t, env, false)
|
|
if err != nil {
|
|
t.Fatalf("in-sync status must exit 0: %v\n%s", err, out)
|
|
}
|
|
for _, want := range []string{"in sync", "deployed revision", "repository revision", "agent-lib"} {
|
|
if !contains(out, want) {
|
|
t.Errorf("status output missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatusDriftNonZero(t *testing.T) {
|
|
h := newHarness(t)
|
|
env := newSyncEnv(t, h)
|
|
buildWorkRepo(t, h, env.workRepo)
|
|
env.writeConfig(t, env.workRepo, "")
|
|
syncOnce(t, env)
|
|
|
|
os.WriteFile(filepath.Join(env.home, ".agents/skills/own-skill/SKILL.md"),
|
|
[]byte("---\nname: Own Skill\n---\n# my edits\n"), 0o644)
|
|
p := filepath.Join(env.workRepo, "skills/own-skill/SKILL.md")
|
|
os.WriteFile(p, []byte("---\nname: Own Skill\ndescription: v2\n---\n# Upstream v2\n"), 0o644)
|
|
commitWorkRepo(t, h, env.workRepo, "change own-skill")
|
|
|
|
out, err := statusRun(t, env, false)
|
|
if err == nil {
|
|
t.Fatalf("blocked-update drift must exit non-zero:\n%s", out)
|
|
}
|
|
for _, want := range []string{"skipped", "own-skill", "locally modified", "drift"} {
|
|
if !contains(out, want) {
|
|
t.Errorf("status missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
|
|
jsonOut, err := statusRun(t, env, true)
|
|
if err == nil {
|
|
t.Fatalf("json status must keep the drift exit code:\n%s", jsonOut)
|
|
}
|
|
var report struct {
|
|
InSync bool `json:"in_sync"`
|
|
Drift bool `json:"problematic_drift"`
|
|
Skipped []struct {
|
|
Item string `json:"item"`
|
|
Reason string `json:"reason"`
|
|
} `json:"skipped_items"`
|
|
Pending []struct {
|
|
Item string `json:"item"`
|
|
} `json:"pending_changes"`
|
|
}
|
|
if err := json.Unmarshal([]byte(jsonOut), &report); err != nil {
|
|
t.Fatalf("status --json not parseable: %v\n%s", err, jsonOut)
|
|
}
|
|
if !report.Drift || report.InSync {
|
|
t.Errorf("json flags wrong: %+v", report)
|
|
}
|
|
if len(report.Skipped) != 1 || report.Skipped[0].Item != "skills/own-skill" {
|
|
t.Errorf("skipped item missing from json: %+v", report.Skipped)
|
|
}
|
|
}
|
|
|
|
func TestStatusUnrecognizedButNoDrift(t *testing.T) {
|
|
h := newHarness(t)
|
|
env := newSyncEnv(t, h)
|
|
buildWorkRepo(t, h, env.workRepo)
|
|
env.writeConfig(t, env.workRepo, "")
|
|
syncOnce(t, env)
|
|
|
|
mine := filepath.Join(env.home, ".agents/skills/my-own-thing")
|
|
os.MkdirAll(mine, 0o755)
|
|
os.WriteFile(filepath.Join(mine, "SKILL.md"), []byte("---\nname: Mine\n---\n"), 0o644)
|
|
|
|
out, err := statusRun(t, env, false)
|
|
if err != nil {
|
|
t.Fatalf("unrecognized items alone are not problematic drift:\n%s", out)
|
|
}
|
|
if !contains(out, "unrecognized local items") || !contains(out, "my-own-thing") {
|
|
t.Errorf("unrecognized item must be listed:\n%s", out)
|
|
}
|
|
if contains(out, "drift:") {
|
|
t.Errorf("no drift line expected:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestStatusNeverSynced(t *testing.T) {
|
|
h := newHarness(t)
|
|
env := newSyncEnv(t, h)
|
|
buildWorkRepo(t, h, env.workRepo)
|
|
env.writeConfig(t, env.workRepo, "")
|
|
|
|
out, err := statusRun(t, env, true)
|
|
if err != nil {
|
|
t.Fatalf("fresh status must exit 0: %v\n%s", err, out)
|
|
}
|
|
var report struct {
|
|
DeployedRev string `json:"deployed_revision"`
|
|
Pending []struct {
|
|
Item string `json:"item"`
|
|
} `json:"pending_changes"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &report); err != nil {
|
|
t.Fatalf("json not parseable: %v", err)
|
|
}
|
|
if report.DeployedRev != "" {
|
|
t.Errorf("never synced must have empty deployed revision: %q", report.DeployedRev)
|
|
}
|
|
if len(report.Pending) == 0 {
|
|
t.Error("fresh status must list everything as pending deploy")
|
|
}
|
|
}
|