Fix sort arg order, glob tests; add test suite (26 tests passing)

ober

5461ef58d6ea53c8677e7110b4425bdeb667f9b8

diff --git a/lib/jcode/tool/file.sls b/lib/jcode/tool/file.sls
index eaf0d42..9215e65 100644
--- a/lib/jcode/tool/file.sls
+++ b/lib/jcode/tool/file.sls
@@ -103,7 +103,7 @@
                                                (string-ref e 0)
                                                #\.))))
                                   entries))]
-                   [sorted (sort visible string<?)]
+                   [sorted (sort string<? visible)]
                    [base (strip-trailing-slash path)]
                    [lines (map (lambda (e)
                                  (let ([full (string-append base "/" e)])
diff --git a/src/jcode/tool/file.ss b/src/jcode/tool/file.ss
index 6f64492..3656b11 100644
--- a/src/jcode/tool/file.ss
+++ b/src/jcode/tool/file.ss
@@ -66,7 +66,7 @@
                                    (or (= (string-length e) 0)
                                        (not (char=? (string-ref e 0) #\.))))
                                  entries)))
-              (sorted  (sort visible string<?))
+              (sorted  (sort string<? visible))
               (base    (strip-trailing-slash path))
               (lines   (map (lambda (e)
                               (let ((full (string-append base "/" e)))
diff --git a/test/run.ss b/test/run.ss
new file mode 100644
index 0000000..f46c7a2
--- /dev/null
+++ b/test/run.ss
@@ -0,0 +1,161 @@
+#!/usr/bin/env scheme --script
+;;; jcode test suite
+
+(import (chezscheme)
+        (jcode core log)
+        (jcode core message)
+        (jcode tool registry)
+        (jcode tool file)
+        (jcode tool bash))
+
+;; ── Helpers ──────────────────────────────────────────────────────
+
+(define pass-count 0)
+(define fail-count 0)
+
+(define (check! desc result expected)
+  (if (equal? result expected)
+    (begin (set! pass-count (+ pass-count 1))
+           (printf "  PASS: ~a~n" desc))
+    (begin (set! fail-count (+ fail-count 1))
+           (printf "  FAIL: ~a~n    expected: ~s~n    got:      ~s~n" desc expected result))))
+
+(define (check-pred! desc result pred)
+  (if (pred result)
+    (begin (set! pass-count (+ pass-count 1))
+           (printf "  PASS: ~a~n" desc))
+    (begin (set! fail-count (+ fail-count 1))
+           (printf "  FAIL: ~a~n    got: ~s~n" desc result))))
+
+(define (section name) (printf "~n~a~n" name))
+
+;; Build an args hashtable from flat key/value pairs
+(define (args . pairs)
+  (let ([h (make-hashtable equal-hash equal?)])
+    (let loop ([p pairs])
+      (unless (null? p)
+        (hashtable-set! h (car p) (cadr p))
+        (loop (cddr p))))
+    h))
+
+(define (str-contains? s needle)
+  (let ([slen (string-length s)] [nlen (string-length needle)])
+    (let loop ([i 0])
+      (cond [(> (+ i nlen) slen) #f]
+            [(string=? (substring s i (+ i nlen)) needle) #t]
+            [else (loop (+ i 1))]))))
+
+(define (str-prefix? p s)
+  (and (>= (string-length s) (string-length p))
+       (string=? (substring s 0 (string-length p)) p)))
+
+(define (string-trim-right s)
+  (let loop ([i (string-length s)])
+    (if (and (> i 0) (char-whitespace? (string-ref s (- i 1))))
+      (loop (- i 1))
+      (substring s 0 i))))
+
+;; ── Setup ─────────────────────────────────────────────────────────
+
+(current-log-level 'warn)
+(init-file-tools)
+(init-bash-tool)
+
+;; ── Message tests ─────────────────────────────────────────────────
+
+(section "=== message ===")
+
+(let ([m (make-user-message "hello")])
+  (check! "user role"    (message-role m)    "user")
+  (check! "user content" (message-content m) "hello")
+  (check! "user no tcs"  (message-tool-calls m) #f))
+
+(let ([m (make-assistant-message "hi")])
+  (check! "assistant role"    (message-role m)    "assistant")
+  (check! "assistant content" (message-content m) "hi"))
+
+(let ([tc (make-tool-call "read" "{\"path\":\"foo\"}")])
+  (check! "tool-call name" (tool-call-name tc)      "read")
+  (check! "tool-call args" (tool-call-arguments tc) "{\"path\":\"foo\"}")
+  (check-pred! "tool-call id non-empty" (tool-call-id tc)
+    (lambda (s) (> (string-length s) 0))))
+
+(let ([tc (restore-tool-call "my-id" "bash" "{}")])
+  (check! "restore-tool-call id" (tool-call-id tc) "my-id"))
+
+(let ([m (make-tool-result "call-123" "result")])
+  (check! "tool-result role"    (message-role m)         "tool")
+  (check! "tool-result content" (message-content m)      "result")
+  (check! "tool-result call-id" (message-tool-call-id m) "call-123"))
+
+;; ── File tool tests ───────────────────────────────────────────────
+
+(section "=== file tools ===")
+
+;; write + read round-trip
+(let* ([tmp  "/tmp/jcode-test-write.txt"]
+       [data "hello\nworld\n"]
+       [_    (tool-execute "write" (args "path" tmp "content" data))]
+       [r    (tool-execute "read"  (args "path" tmp))])
+  (check! "write+read round-trip" r data))
+
+;; read missing file
+(let ([r (tool-execute "read" (args "path" "/tmp/jcode-no-such-file-xyz-abc"))])
+  (check-pred! "read missing returns error" r (lambda (s) (str-prefix? "Error:" s))))
+
+;; edit
+(let* ([tmp "/tmp/jcode-test-edit.txt"]
+       [_   (tool-execute "write" (args "path" tmp "content" "foo bar baz"))]
+       [_   (tool-execute "edit"  (args "path" tmp "old_str" "bar" "new_str" "QUX"))]
+       [r   (tool-execute "read"  (args "path" tmp))])
+  (check! "edit replaces string" r "foo QUX baz"))
+
+;; edit missing string
+(let* ([tmp "/tmp/jcode-test-edit2.txt"]
+       [_   (tool-execute "write" (args "path" tmp "content" "abc"))]
+       [r   (tool-execute "edit"  (args "path" tmp "old_str" "zzz" "new_str" "q"))])
+  (check-pred! "edit missing string returns error" r (lambda (s) (str-prefix? "Error:" s))))
+
+;; glob
+(let ([r (tool-execute "glob" (args "pattern" "*.ss" "path" (current-directory)))])
+  (check-pred! "glob finds root .ss files" r
+    (lambda (s) (str-contains? s "main.ss"))))
+
+(let ([r (tool-execute "glob" (args "pattern" "**/*.ss" "path" (current-directory)))])
+  (check-pred! "glob finds src .ss files" r
+    (lambda (s) (str-contains? s ".ss"))))
+
+;; ls
+(let ([r (tool-execute "ls" (args "path" "."))])
+  (check-pred! "ls lists src dir" r (lambda (s) (str-contains? s "src")))
+  (check-pred! "ls marks dirs with /" r (lambda (s) (str-contains? s "src/"))))
+
+(let ([r (tool-execute "ls" (args "path" "/tmp/no-such-dir-xyz"))])
+  (check-pred! "ls missing dir error" r (lambda (s) (str-prefix? "Error:" s))))
+
+;; grep
+(let ([r (tool-execute "grep" (args "pattern" "cli-main"
+                                    "path"    "src/jcode/ui/cli.ss"))])
+  (check-pred! "grep finds pattern" r (lambda (s) (str-contains? s "cli-main"))))
+
+(let ([r (tool-execute "grep" (args "pattern" "zzz_not_found_zzz"
+                                    "path"    "src/jcode/ui/cli.ss"))])
+  (check! "grep no match" r "No matches found"))
+
+;; ── Bash tool tests ───────────────────────────────────────────────
+
+(section "=== bash tool ===")
+
+(let ([r (tool-execute "bash" (args "command" "echo hello"))])
+  (check! "bash echo" (string-trim-right r) "hello"))
+
+(let ([r (tool-execute "bash" (args "command" "echo -n abc && echo def"))])
+  (check-pred! "bash multi-cmd" r (lambda (s) (str-contains? s "abc"))))
+
+(let ([r (tool-execute "bash" (args "command" "exit 42"))])
+  (check-pred! "bash non-zero exit shown" r (lambda (s) (str-contains? s "Exit code"))))
+
+;; ── Results ───────────────────────────────────────────────────────
+
+(printf "~n~a passed, ~a failed~n" pass-count fail-count)
+(when (> fail-count 0) (exit 1))