Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/odek/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,17 @@ func auditCmd(args []string) error {
printAuditUsage()
return nil
case "--list":
if len(args) != 1 {
return fmt.Errorf("audit: usage: odek audit --list")
}
return auditList(store, auditStore)
default:
if strings.HasPrefix(args[0], "-") {
return fmt.Errorf("audit: unknown flag %q (use --list or a session id)", args[0])
}
if len(args) != 1 {
return fmt.Errorf("audit: usage: odek audit <session-id> | --list")
}
log, err := auditStore.Load(args[0])
if err != nil {
return fmt.Errorf("audit: load: %w", err)
Expand Down
21 changes: 21 additions & 0 deletions cmd/odek/cli_audit_flag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"strings"
"testing"
)

// From the 2026-10-13 CLI contract audit: every command was run against
// its documented contract.

// `odek audit --bogus` is an unknown flag, not a session called "--bogus".
func TestRED_AuditCmd_UnknownFlagRejected(t *testing.T) {
withTempHome(t)
err := auditCmd([]string{"--bogus"})
if err == nil || !strings.Contains(err.Error(), "unknown flag") {
t.Fatalf("audit --bogus: %v; want an unknown-flag error", err)
}
if err := auditCmd([]string{"--list", "extra"}); err == nil || !strings.Contains(err.Error(), "usage") {
t.Fatalf("audit --list extra: %v; want a usage error", err)
}
}
28 changes: 28 additions & 0 deletions cmd/odek/cli_help_alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"strings"
"testing"
)

// From the 2026-10-13 CLI contract audit: every command was run against
// its documented contract.

// `odek help`, `--help` and `-h` print the usage and exit 0; they were
// "unknown command", exit 1.
func TestRED_Dispatch_HelpAliasesExitZero(t *testing.T) {
for _, arg := range []string{"help", "--help", "-h"} {
flush := captureStderr(t)
out := captureStdout(func() {
if code := dispatch([]string{arg}); code != 0 {
t.Errorf("odek %s: exit %d, want 0", arg, code)
}
})
if stderr := flush(); strings.Contains(stderr, "unknown command") {
t.Errorf("odek %s treated as unknown command:\n%s", arg, stderr)
}
if !strings.Contains(out, "Usage:") {
t.Errorf("odek %s printed no usage:\n%s", arg, out)
}
}
}
32 changes: 32 additions & 0 deletions cmd/odek/cli_logs_help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"strings"
"testing"
)

// From the 2026-10-13 CLI contract audit: every command was run against
// its documented contract.

// `odek logs --help` prints the usage and exits 0; an unknown flag is an
// error in the odek style, not the flag package's "Usage of logs:" dump.
func TestRED_LogsCmd_HelpAndUnknownFlag(t *testing.T) {
setupTestHome(t)
out := captureStdout(func() {
if err := logsCmd([]string{"--help"}); err != nil {
t.Errorf("logs --help: %v", err)
}
})
if !strings.Contains(out, "Usage: odek logs") {
t.Errorf("logs --help usage:\n%s", out)
}
flush := captureStderr(t)
err := logsCmd([]string{"--bogus"})
stderr := flush()
if err == nil || !strings.Contains(err.Error(), "--bogus") {
t.Fatalf("logs --bogus: %v", err)
}
if strings.Contains(stderr, "Usage of logs") {
t.Errorf("flag package usage dump leaked:\n%s", stderr)
}
}
109 changes: 109 additions & 0 deletions cmd/odek/cli_session_shape_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"errors"
"os"
"path/filepath"
"strings"
"testing"

"github.com/BackendStack21/odek/internal/session"
)

// From the 2026-10-13 CLI contract audit: every command was run against
// its documented contract.

// Deleting a session that does not exist is an error, not "Deleted session".
func TestRED_SessionDelete_UnknownIDIsAnError(t *testing.T) {
home := setupTestHome(t)
store, err := session.NewStore()
if err != nil {
t.Fatal(err)
}
out := captureStdout(func() {
err = deleteSession(store, []string{"20260101-doesnotexist"})
})
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("delete unknown session: err=%v out=%q; want a not-found error", err, out)
}
if strings.Contains(out, "Deleted") {
t.Fatalf("reported success for a missing session: %q", out)
}
// A session whose file is gone but whose audit log remains is purged
// (the audit log must not outlive a deleted session).
auditDir := filepath.Join(home, ".odek", "sessions", "audit")
os.MkdirAll(auditDir, 0700)
os.WriteFile(filepath.Join(auditDir, "20260101-leftover.json"), []byte("{}"), 0600)
out = captureStdout(func() { err = deleteSession(store, []string{"20260101-leftover"}) })
if err != nil || !strings.Contains(out, "leftovers") {
t.Fatalf("purge leftovers: err=%v out=%q", err, out)
}
if _, statErr := os.Stat(filepath.Join(auditDir, "20260101-leftover.json")); !os.IsNotExist(statErr) {
t.Fatal("audit log of the deleted session still exists")
}
}

// Loading a missing session says "not found" instead of leaking the store
// path through a raw open error; os.ErrNotExist is still reachable for
// callers that test for it.
func TestRED_SessionLoad_MissingIsPlainNotFound(t *testing.T) {
setupTestHome(t)
store, err := session.NewStore()
if err != nil {
t.Fatal(err)
}
_, err = store.Load("20260101-doesnotexist")
if err == nil {
t.Fatal("expected an error")
}
if !strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), string(os.PathSeparator)+"sessions") {
t.Fatalf("load error = %q; want a plain not-found message without the store path", err)
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatal("not-found error no longer matches os.ErrNotExist")
}
// The CLI surfaces the same message.
if err := showSession(store, []string{"20260101-doesnotexist"}); err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("session show: %v", err)
}
}

// `--max-iter 0` (or negative) is an error like every other numeric budget
// flag; it was silently ignored.
func TestRED_RunFlags_MaxIterMustBePositive(t *testing.T) {
for _, v := range []string{"0", "-3"} {
if _, err := parseRunFlags([]string{"--max-iter", v, "task"}); err == nil || !strings.Contains(err.Error(), "positive") {
t.Errorf("--max-iter %s: %v; want a positive-integer error", v, err)
}
}
}

// Fixed-shape listing commands reject stray arguments instead of ignoring
// them, and the session usage names every subcommand.
func TestRED_ListingsRejectStrayArguments(t *testing.T) {
home := setupTestHome(t)
os.MkdirAll(filepath.Join(home, ".odek"), 0700)
for _, args := range [][]string{{"list", "extra"}, {"show", "a", "b"}, {"delete", "a", "b"}, {"trim", "a", "5", "c"}, {"cleanup", "5", "x"}} {
if err := sessionCmd(args); err == nil || !strings.Contains(err.Error(), "usage") {
t.Errorf("session %s: %v; want a usage error", strings.Join(args, " "), err)
}
}
for _, args := range [][]string{{"list", "extra"}, {"rm", "a", "b"}, {"enable", "a", "b"}, {"disable", "a", "b"}, {"next", "a", "b"}, {"run", "a", "b"}, {"daemon", "--bogus"}} {
if err := scheduleCmd(args); err == nil || !strings.Contains(err.Error(), "usage") {
t.Errorf("schedule %s: %v; want a usage error", strings.Join(args, " "), err)
}
}
for _, args := range [][]string{{"list", "extra"}, {"view", "a", "b"}, {"delete", "a", "b"}, {"promote", "a", "--froce"}, {"import", "file:///x.md", "--bogus"}} {
if err := skillCmd(args); err == nil || !strings.Contains(err.Error(), "usage") {
t.Errorf("skill %s: %v; want a usage error", strings.Join(args, " "), err)
}
}
flush := captureStderr(t)
_ = sessionCmd(nil)
usage := flush()
for _, want := range []string{"trim", "cleanup"} {
if !strings.Contains(usage, want) {
t.Errorf("session usage lacks %q:\n%s", want, usage)
}
}
}
27 changes: 27 additions & 0 deletions cmd/odek/cli_subagent_clamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"strings"
"testing"
)

// From the 2026-10-13 CLI contract audit: every command was run against
// its documented contract.

// Sub-agent caps are documented as limits; a value above them is clamped
// and the clamp is reported on stderr instead of happening silently.
func TestRED_SubagentFlags_ClampIsReported(t *testing.T) {
flush := captureStderr(t)
cfg, err := parseSubagentFlags([]string{"--goal", "x", "--timeout", "99999", "--max-iter", "500"})
stderr := flush()
if err != nil {
t.Fatal(err)
}
if cfg.timeout != 1800 || cfg.maxIter != 100 {
t.Fatalf("clamp: timeout=%d maxIter=%d", cfg.timeout, cfg.maxIter)
}
if !strings.Contains(stderr, "--timeout 99999 exceeds the sub-agent cap, clamped to 1800") ||
!strings.Contains(stderr, "--max-iter 500 exceeds the sub-agent cap, clamped to 100") {
t.Fatalf("clamp not reported on stderr:\n%s", stderr)
}
}
33 changes: 33 additions & 0 deletions cmd/odek/cli_telegram_args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

// From the 2026-10-13 CLI contract audit: every command was run against
// its documented contract.

// `odek telegram` takes no arguments; before, `odek telegram --bogus`
// ignored the argument and started polling with the configured token.
func TestRED_TelegramCmd_RejectsArguments(t *testing.T) {
home := setupTestHome(t)
err := telegramCmd([]string{"--bogus"})
if err == nil || !strings.Contains(err.Error(), "takes no arguments") {
t.Fatalf("telegram --bogus: %v; want a usage error", err)
}
// Rejected before the singleton lock and the config are touched.
if _, statErr := os.Stat(filepath.Join(home, ".odek", "telegram.lock")); statErr == nil {
t.Fatal("argument check ran after the singleton lock was taken")
}
out := captureStdout(func() {
if err := telegramCmd([]string{"--help"}); err != nil {
t.Errorf("telegram --help: %v", err)
}
})
if !strings.Contains(out, "Usage: odek telegram") {
t.Errorf("telegram --help usage:\n%s", out)
}
}
3 changes: 3 additions & 0 deletions cmd/odek/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func dispatch(args []string) int {
switch cmd {
case "run":
return runExit(run(rest))
case "help", "--help", "-h":
printUsage()
return 0
case "version", "--version", "-v":
// --version is the form every packaging script, CI preflight, and
// support-bundle collector reaches for first; treat it as a full
Expand Down
25 changes: 23 additions & 2 deletions cmd/odek/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import (
"unicode"

"github.com/BackendStack21/odek/internal/config"
"github.com/BackendStack21/odek/internal/danger"
"github.com/BackendStack21/odek/internal/logquery"
)

func logsCmd(args []string) error {
fs := flag.NewFlagSet("logs", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
fs.SetOutput(io.Discard) // errors are reported in the odek style below
fs.Usage = func() {}
level := fs.String("level", "", "minimum level: debug, info, warn, error")
since := fs.String("since", "", "lower time bound (duration ago or RFC3339)")
until := fs.String("until", "", "upper time bound (RFC3339)")
Expand All @@ -38,7 +40,14 @@ func logsCmd(args []string) error {
limit := fs.Int("limit", 100, "maximum initial records (0 means unlimited)")
path := fs.String("file", "", "runtime log path override")
if err := fs.Parse(args); err != nil {
return err
if errors.Is(err, flag.ErrHelp) {
printLogsUsage(fs)
return nil
}
if name, ok := strings.CutPrefix(err.Error(), "flag provided but not defined: -"); ok {
return fmt.Errorf("unknown flag %q for logs (see: odek logs --help)", "--"+name)
}
return fmt.Errorf("%s (see: odek logs --help)", danger.SanitizeInline(err.Error()))
}
if fs.NArg() != 0 {
return fmt.Errorf("unexpected argument %q", fs.Arg(0))
Expand Down Expand Up @@ -439,3 +448,15 @@ func statCursor(p string) (fileCursor, bool) {
func openLogRead(p string) (*os.File, error) {
return os.OpenFile(p, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0)
}

// printLogsUsage prints the logs usage and flag defaults to stdout.
func printLogsUsage(fs *flag.FlagSet) {
fmt.Println(`Usage: odek logs [filters]

Query, follow, or display the local operational log (see docs/LOGGING.md).

Filters:`)
fs.SetOutput(os.Stdout)
fs.PrintDefaults()
fs.SetOutput(io.Discard)
}
Loading
Loading