feat(auto_context): port to FULL Go — dichotomy wave
Full name-preserving port of src/rune/auto_context.py to src/go/auto_context (GO verdict, #2479 dichotomy tranche — 8/8 fns, dichotomy audit match clean): - run_warm_router / run_memory_recall / run_always_digest: subprocess seam (RunScript) with the oracle's exact degrade paths (nonzero exit, timeout, bad JSON, empty stdout → None; non-list recall payload → []), budget truncation data[:budget] - load_active_tasks: persistence-ledger filter (open/in_progress) with every best-effort degrade branch (missing file, malformed JSON, non-dict ledger, non-dict task → []) - build_auto_context: stopword table 1:1, len>4 + isalpha + first-5 query extraction, 200-rune preview truncation, Python truthiness on every optional payload, frozen-clock isoformat - save_context: atomic tmp+rename, json.dumps(indent=2, ensure_ascii) byte-parity via pyjson.go - handle_hook: message/text/str(data) fallback chain (Python dict repr included), both best-effort error dicts verbatim ('list' object has no attribute 'get' / object of type 'int' has no len()) - CLI main(): argparse-compatible build/show/hook (--event/--data/--json), byte-parity stdout, exit codes (invalid choice/missing cmd → 2) - PLUGIN manifest pinned (pluginManifest) - Battery: 52 frozen truth rows vs the LIVE oracle (clock frozen, env-driven stub scripts echo the exact message/query the port hands the seam), deterministic across two generator runs; replay gate + TestTruthParity
This commit is contained in:
1 parent
72f70aa47a
commit
f129a6a469
6 files changed
+3194
No files matched your search
@@ -0,0 +1,3 @@
|
||||
module rune/auto_context
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,775 @@
|
||||
// Package main — the FULL GO port of src/rune/auto_context.py (#2479
|
||||
// dichotomy tranche, GO side verdict). Every public Python function has a
|
||||
// name-preserving counterpart: run_warm_router → runWarmRouter,
|
||||
// run_memory_recall → runMemoryRecall, run_always_digest → runAlwaysDigest,
|
||||
// load_active_tasks → loadActiveTasks, build_auto_context →
|
||||
// buildAutoContext, save_context → saveContext, handle_hook → handleHook,
|
||||
// and the module CLI main() (argparse-compatible: build/show/hook, --event,
|
||||
// --data, --json; invalid choices exit 2). The PLUGIN manifest rides along
|
||||
// as pluginManifest().
|
||||
//
|
||||
// IO (subprocess runs, memory paths, wall clock) stays behind injectable
|
||||
// seams so the frozen truth battery replays deterministically: RunScript,
|
||||
// MemoryDir, ScriptsDir, PythonBin and ClockNow are swappable vars, swapped
|
||||
// per truth row by replay.go and restored after each row. The oracle's
|
||||
// subprocess payloads (warm_router.py route/always, memory_store.py recall)
|
||||
// arrive through the RunScript seam; the frozen truth pins the exact
|
||||
// message/query strings the port hands to that seam.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ── configuration ────────────────────────────────────────────────────────
|
||||
|
||||
// stopwordSet — the oracle's recall stopword table, 1:1.
|
||||
var stopwordSet = map[string]struct{}{
|
||||
"what": {}, "when": {}, "where": {}, "which": {}, "that": {}, "this": {},
|
||||
"with": {}, "from": {}, "have": {}, "been": {}, "were": {}, "they": {},
|
||||
"their": {}, "about": {}, "would": {}, "could": {}, "should": {},
|
||||
"will": {}, "into": {}, "your": {}, "just": {}, "like": {}, "know": {},
|
||||
"think": {}, "also": {}, "back": {}, "then": {}, "than": {}, "more": {},
|
||||
"some": {}, "only": {}, "very": {}, "does": {}, "done": {}, "make": {},
|
||||
"made": {}, "want": {}, "need": {}, "give": {}, "tell": {}, "there": {},
|
||||
"these": {}, "those": {}, "because": {}, "between": {}, "after": {},
|
||||
"before": {}, "under": {}, "over": {}, "again": {}, "once": {},
|
||||
"here": {}, "each": {}, "every": {}, "both": {}, "few": {}, "most": {},
|
||||
"other": {}, "such": {}, "same": {}, "being": {}, "having": {},
|
||||
}
|
||||
|
||||
// ── seams (swapped per truth row by replay.go) ───────────────────────────
|
||||
|
||||
// ExecResult — the slice of subprocess.run(capture_output=True, text=True)
|
||||
// the oracle consumes: stdout text, return code, and a non-nil Err when the
|
||||
// run failed to start, was killed, or hit its timeout (subprocess raised).
|
||||
type ExecResult struct {
|
||||
Stdout string
|
||||
ReturnCode int
|
||||
Err error
|
||||
}
|
||||
|
||||
var (
|
||||
// defaultPythonBin — memory_env.python_bin(): sys.executable of the
|
||||
// parent process (same venv, portable by construction).
|
||||
defaultPythonBin = func() string {
|
||||
if p := os.Getenv("RUNE_PYTHON"); p != "" {
|
||||
return p
|
||||
}
|
||||
return "python3"
|
||||
}
|
||||
// defaultScriptsDir — memory_env.scripts_dir() (RUNE_SCRIPTS_DIR).
|
||||
defaultScriptsDir = func() string {
|
||||
if d := os.Getenv("RUNE_SCRIPTS_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
return "src/rune"
|
||||
}
|
||||
// defaultMemoryDir — memory_env.memory_dir() (RUNE_MEMORY_DIR else
|
||||
// RUNE_HOME/memory else the default rune home).
|
||||
defaultMemoryDir = func() string {
|
||||
if d := os.Getenv("RUNE_MEMORY_DIR"); d != "" {
|
||||
return d
|
||||
}
|
||||
home := os.Getenv("RUNE_HOME")
|
||||
if home == "" {
|
||||
home = "/mnt/workdrive/rune"
|
||||
}
|
||||
return filepath.Join(home, "memory")
|
||||
}
|
||||
// defaultClockNow — datetime.now(timezone.utc); build_auto_context
|
||||
// calls it once per build.
|
||||
defaultClockNow = func() time.Time { return time.Now().UTC() }
|
||||
// defaultRunScript — subprocess.run([python, script, ...], timeout=N):
|
||||
// timeout expiry surfaces as Err (subprocess.TimeoutExpired), a
|
||||
// nonzero exit keeps Stdout and sets ReturnCode.
|
||||
defaultRunScript = func(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, exe, args...)
|
||||
out, err := cmd.Output()
|
||||
res := ExecResult{Stdout: string(out)}
|
||||
if err != nil {
|
||||
res.Err = err
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
res.ReturnCode = ee.ExitCode()
|
||||
} else {
|
||||
res.ReturnCode = -1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
PythonBin = defaultPythonBin
|
||||
ScriptsDir = defaultScriptsDir
|
||||
MemoryDir = defaultMemoryDir
|
||||
ClockNow = defaultClockNow
|
||||
RunScript = defaultRunScript
|
||||
)
|
||||
|
||||
func warmRouterPath() string { return filepath.Join(ScriptsDir(), "warm_router.py") }
|
||||
func memoryStorePath() string {
|
||||
return filepath.Join(ScriptsDir(), "memory_store.py")
|
||||
}
|
||||
func contextFilePath() string {
|
||||
return filepath.Join(MemoryDir(), "index", "auto_context.json")
|
||||
}
|
||||
func tasksFilePath() string {
|
||||
return filepath.Join(MemoryDir(), "tasks", "persisted.json")
|
||||
}
|
||||
|
||||
// isoformat — datetime.isoformat() on an aware UTC datetime: fractional
|
||||
// part only when microseconds are nonzero, offset always "+00:00" shape.
|
||||
func isoformat(t time.Time) string {
|
||||
base := t.Format("2006-01-02T15:04:05")
|
||||
if us := t.Nanosecond() / 1000; us != 0 {
|
||||
base += fmt.Sprintf(".%06d", us)
|
||||
}
|
||||
return base + t.Format("-07:00")
|
||||
}
|
||||
|
||||
// ── Python value semantics helpers ───────────────────────────────────────
|
||||
|
||||
// pyTruthy — Python truthiness for JSON-decoded values: None, False, 0,
|
||||
// "" and every empty container are falsy.
|
||||
func pyTruthy(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return t
|
||||
case string:
|
||||
return t != ""
|
||||
case float64:
|
||||
return t != 0
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f != 0
|
||||
case []any:
|
||||
return len(t) > 0
|
||||
case []*OrderedMap:
|
||||
return len(t) > 0
|
||||
case *OrderedMap:
|
||||
return t != nil && t.Len() > 0
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// pyTypeName — Python type() name for JSON-decoded values (integral
|
||||
// floats decode as 'int', mirroring json.loads producing ints).
|
||||
func pyTypeName(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "NoneType"
|
||||
case bool:
|
||||
return "bool"
|
||||
case string:
|
||||
return "str"
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return "int"
|
||||
}
|
||||
return "float"
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
if f == float64(int64(f)) && !strings.ContainsAny(t.String(), ".eE") {
|
||||
return "int"
|
||||
}
|
||||
return "float"
|
||||
case []any, []*OrderedMap:
|
||||
return "list"
|
||||
case *OrderedMap:
|
||||
return "dict"
|
||||
default:
|
||||
return "object"
|
||||
}
|
||||
}
|
||||
|
||||
// pyIsAlpha — str.isalpha(): non-empty and every rune a Unicode letter.
|
||||
func pyIsAlpha(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if !unicode.IsLetter(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// runeSlice — Python's s[:n] (codepoint slice).
|
||||
func runeSlice(s string, n int) string {
|
||||
if utf8.RuneCountInString(s) <= n {
|
||||
return s
|
||||
}
|
||||
return string([]rune(s)[:n])
|
||||
}
|
||||
|
||||
func truncateRunes(s string, n int) string { return runeSlice(s, n) }
|
||||
|
||||
// pyParse — json.loads with order-preserving objects and json.Number
|
||||
// numerals (so ints survive byte-parity re-encoding).
|
||||
func pyParse(s string) (any, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(s))
|
||||
dec.UseNumber()
|
||||
return decodeJSONValue(dec)
|
||||
}
|
||||
|
||||
// pythonRepr — repr() of a JSON-decoded value (the handle_hook
|
||||
// str(data) fallback), Python quoting rules included.
|
||||
func pythonRepr(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "None"
|
||||
case bool:
|
||||
if t {
|
||||
return "True"
|
||||
}
|
||||
return "False"
|
||||
case string:
|
||||
return pythonReprString(t)
|
||||
case json.Number:
|
||||
if !strings.ContainsAny(t.String(), ".eE") {
|
||||
return t.String()
|
||||
}
|
||||
f, _ := t.Float64()
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'g', -1, 64)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = pythonRepr(item)
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case *OrderedMap:
|
||||
entries := t.Entries()
|
||||
parts := make([]string, 0, len(entries))
|
||||
for _, kv := range entries {
|
||||
parts = append(parts, pythonReprString(kv.Key)+": "+pythonRepr(kv.Value))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
default:
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
|
||||
// pythonReprString — repr() of a str: single quotes preferred, double
|
||||
// quotes when the string contains ' but not ", backslash and quote
|
||||
// escapes, \n \r \t shortcuts.
|
||||
func pythonReprString(s string) string {
|
||||
escape := func(quote byte) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte(quote)
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\'':
|
||||
if quote == '\'' {
|
||||
sb.WriteString(`\'`)
|
||||
} else {
|
||||
sb.WriteByte('\'')
|
||||
}
|
||||
case '"':
|
||||
if quote == '"' {
|
||||
sb.WriteString(`\"`)
|
||||
} else {
|
||||
sb.WriteByte('"')
|
||||
}
|
||||
default:
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
sb.WriteByte(quote)
|
||||
return sb.String()
|
||||
}
|
||||
if strings.Contains(s, "'") && !strings.Contains(s, "\"") {
|
||||
return escape('"')
|
||||
}
|
||||
return escape('\'')
|
||||
}
|
||||
|
||||
// ── the port (1:1 with the oracle) ───────────────────────────────────────
|
||||
|
||||
// runWarmRouter — run_warm_router: route the message through the warm
|
||||
// router subprocess and return the fired-neurons payload; ANY failure
|
||||
// (nonzero exit, timeout, bad JSON, empty stdout) degrades to None.
|
||||
func runWarmRouter(message string) any {
|
||||
res := RunScript(PythonBin(), []string{warmRouterPath(), "route", message, "--json"}, 10*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runMemoryRecall — run_memory_recall: recall relevant entries; a
|
||||
// non-list payload degrades to [], a list is capped at budget entries.
|
||||
func runMemoryRecall(query string, budget int) []any {
|
||||
res := RunScript(PythonBin(), []string{memoryStorePath(), "recall", query, "--json"}, 15*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
if list, ok := v.([]any); ok {
|
||||
if budget >= 0 && budget < len(list) {
|
||||
return list[:budget]
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
}
|
||||
return []any{}
|
||||
}
|
||||
|
||||
// runAlwaysDigest — run_always_digest: the always-on digest payload or
|
||||
// None on any failure.
|
||||
func runAlwaysDigest() any {
|
||||
res := RunScript(PythonBin(), []string{warmRouterPath(), "always", "--json"}, 10*time.Second)
|
||||
if res.Err == nil && res.ReturnCode == 0 && strings.TrimSpace(res.Stdout) != "" {
|
||||
if v, err := pyParse(res.Stdout); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadActiveTasks — load_active_tasks: read the persistence ledger and
|
||||
// keep open/in_progress tasks; missing file, malformed JSON, a non-dict
|
||||
// ledger, a non-dict task entry — every failure degrades to [].
|
||||
func loadActiveTasks() []any {
|
||||
raw, err := os.ReadFile(tasksFilePath())
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
v, err := pyParse(string(raw))
|
||||
if err != nil {
|
||||
return []any{}
|
||||
}
|
||||
ledger, ok := v.(*OrderedMap)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
tasks, ok := ledger.Get("tasks")
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
list, ok := tasks.([]any)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
active := []any{}
|
||||
for _, t := range list {
|
||||
tom, ok := t.(*OrderedMap)
|
||||
if !ok {
|
||||
return []any{} // t.get on a non-dict raises → the oracle's except
|
||||
}
|
||||
status, _ := tom.Get2("status").(string)
|
||||
if status == "open" || status == "in_progress" {
|
||||
active = append(active, tom)
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
// recallQueryWords — the oracle's key-term extraction: lowercase, split on
|
||||
// whitespace, keep words longer than 4 chars that are pure alphabetic and
|
||||
// not stopwords.
|
||||
func recallQueryWords(message string) []string {
|
||||
words := []string{}
|
||||
for _, w := range strings.Fields(strings.ToLower(message)) {
|
||||
if utf8.RuneCountInString(w) > 4 && pyIsAlpha(w) {
|
||||
if _, stop := stopwordSet[w]; !stop {
|
||||
words = append(words, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
// buildAutoContext — build_auto_context: warm neurons, active tasks,
|
||||
// memory recall (substantive messages only), always-on digest.
|
||||
func buildAutoContext(message string) *OrderedMap {
|
||||
context := NewOrderedMap(
|
||||
KV{"timestamp", isoformat(ClockNow().UTC())},
|
||||
KV{"message_preview", runeSlice(message, 200)},
|
||||
KV{"warm_neurons", nil},
|
||||
KV{"memory_recall", nil},
|
||||
KV{"always_digest", nil},
|
||||
KV{"active_tasks", nil},
|
||||
)
|
||||
|
||||
warm := runWarmRouter(message)
|
||||
if pyTruthy(warm) {
|
||||
context.Set("warm_neurons", warm)
|
||||
}
|
||||
|
||||
tasks := loadActiveTasks()
|
||||
if len(tasks) > 0 {
|
||||
context.Set("active_tasks", tasks)
|
||||
}
|
||||
|
||||
if message != "" && utf8.RuneCountInString(strings.TrimSpace(message)) > 10 {
|
||||
words := recallQueryWords(message)
|
||||
if len(words) > 0 {
|
||||
n := 5
|
||||
if len(words) < n {
|
||||
n = len(words)
|
||||
}
|
||||
recall := runMemoryRecall(strings.Join(words[:n], " "), 5)
|
||||
if len(recall) > 0 {
|
||||
context.Set("memory_recall", recall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
digest := runAlwaysDigest()
|
||||
if pyTruthy(digest) {
|
||||
context.Set("always_digest", digest)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
// saveContext — save_context: atomic write (tmp file + rename) of the
|
||||
// context JSON, indent=2, ensure_ascii.
|
||||
func saveContext(context *OrderedMap) error {
|
||||
if err := os.MkdirAll(filepath.Dir(contextFilePath()), 0o777); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := contextFilePath() + ".tmp"
|
||||
if err := os.WriteFile(tmp, []byte(encodePythonJSON(context, 0)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, contextFilePath())
|
||||
}
|
||||
|
||||
// handleHook — handle_hook: the on_message plugin hook. The truthiness
|
||||
// guard, the message/text/str(data) fallback chain and the two nested
|
||||
// count expressions reproduce the oracle exactly, including its
|
||||
// best-effort error dicts (truncated to 120 runes).
|
||||
func handleHook(event string, data *OrderedMap) *OrderedMap {
|
||||
errorDict := func(msg string) *OrderedMap {
|
||||
return NewOrderedMap(
|
||||
KV{"status", "error"},
|
||||
KV{"event", event},
|
||||
KV{"error", truncateRunes(msg, 120)},
|
||||
)
|
||||
}
|
||||
if event == "on_message" && pyTruthy(data) {
|
||||
message := any("")
|
||||
for _, key := range []string{"message", "text"} {
|
||||
v, ok := data.Get(key)
|
||||
if !ok {
|
||||
v = ""
|
||||
}
|
||||
if pyTruthy(v) {
|
||||
message = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if !pyTruthy(message) {
|
||||
message = pythonRepr(data)
|
||||
}
|
||||
msgStr, isStr := message.(string)
|
||||
if !isStr {
|
||||
return errorDict(fmt.Sprintf("'%s' object has no attribute 'strip'", pyTypeName(message)))
|
||||
}
|
||||
if msgStr != "" && utf8.RuneCountInString(strings.TrimSpace(msgStr)) > 2 {
|
||||
context := buildAutoContext(msgStr)
|
||||
if err := saveContext(context); err != nil {
|
||||
return errorDict(err.Error())
|
||||
}
|
||||
neurons := context.Get2("warm_neurons")
|
||||
recall := context.Get2("memory_recall")
|
||||
tasks := context.Get2("active_tasks")
|
||||
neuronsFired := 0
|
||||
if pyTruthy(neurons) {
|
||||
om, ok := neurons.(*OrderedMap)
|
||||
if !ok {
|
||||
return errorDict(fmt.Sprintf("'%s' object has no attribute 'get'", pyTypeName(neurons)))
|
||||
}
|
||||
fired, _ := om.Get("fired")
|
||||
// .get("fired", []) — a MISSING key defaults to [], a
|
||||
// present-but-None key does not.
|
||||
if fired == nil {
|
||||
if _, present := om.Get("fired"); !present {
|
||||
fired = []any{}
|
||||
}
|
||||
}
|
||||
n, err := pyLen(fired)
|
||||
if err != nil {
|
||||
return errorDict(err.Error())
|
||||
}
|
||||
neuronsFired = n
|
||||
}
|
||||
memoriesRecalled := 0
|
||||
if pyTruthy(recall) {
|
||||
if n, err := pyLen(recall); err != nil {
|
||||
return errorDict(err.Error())
|
||||
} else {
|
||||
memoriesRecalled = n
|
||||
}
|
||||
}
|
||||
activeTasks := 0
|
||||
if pyTruthy(tasks) {
|
||||
if n, err := pyLen(tasks); err != nil {
|
||||
return errorDict(err.Error())
|
||||
} else {
|
||||
activeTasks = n
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_context"},
|
||||
KV{"neurons_fired", neuronsFired},
|
||||
KV{"memories_recalled", memoriesRecalled},
|
||||
KV{"active_tasks", activeTasks},
|
||||
KV{"context_file", contextFilePath()},
|
||||
)
|
||||
}
|
||||
}
|
||||
return NewOrderedMap(
|
||||
KV{"status", "ok"},
|
||||
KV{"event", event},
|
||||
KV{"handler", "auto_context"},
|
||||
)
|
||||
}
|
||||
|
||||
// pyLen — len() for JSON-decoded values; non-sized types raise TypeError
|
||||
// in Python ("object of type 'int' has no len()").
|
||||
func pyLen(v any) (int, error) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return 0, fmt.Errorf("object of type 'NoneType' has no len()")
|
||||
case string:
|
||||
return utf8.RuneCountInString(t), nil
|
||||
case []any:
|
||||
return len(t), nil
|
||||
case []*OrderedMap:
|
||||
return len(t), nil
|
||||
case *OrderedMap:
|
||||
return t.Len(), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("object of type '%s' has no len()", pyTypeName(v))
|
||||
}
|
||||
}
|
||||
|
||||
// pluginManifest — the module-level PLUGIN dict (microkernel discovery
|
||||
// metadata), key order preserved.
|
||||
func pluginManifest() *OrderedMap {
|
||||
return NewOrderedMap(
|
||||
KV{"name", "auto_context"},
|
||||
KV{"version", "1.0.0"},
|
||||
KV{"description", "Auto-context — automatic memory and warm neuron activation on every message."},
|
||||
KV{"capabilities", []any{"context", "memory", "activation"}},
|
||||
KV{"hooks", NewOrderedMap(KV{"on_message", "handle_hook"})},
|
||||
KV{"dependencies", []any{"memory_store", "warm_router"}},
|
||||
KV{"provides", []any{"auto_context"}},
|
||||
KV{"priority", 5},
|
||||
KV{"auto_activate", true},
|
||||
)
|
||||
}
|
||||
|
||||
// ── CLI (auto_context.py main(), byte-parity stdout) ─────────────────────
|
||||
|
||||
func argparseUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage: auto_context.py [-h] [--event EVENT] [--data DATA] [--json] {build,show,hook} [arg]")
|
||||
}
|
||||
|
||||
// RunCLI executes auto_context.py main() against argv (program name at
|
||||
// index 0) and returns the process exit code. argparse failures print
|
||||
// usage to stderr and exit 2.
|
||||
func RunCLI(argv []string, stdout io.Writer) int {
|
||||
stderr := os.Stderr
|
||||
var positionals []string
|
||||
event, hasEvent := "", false
|
||||
dataStr, hasData := "", false
|
||||
jsonFlag := false
|
||||
i := 1
|
||||
for i < len(argv) {
|
||||
a := argv[i]
|
||||
switch {
|
||||
case a == "--json":
|
||||
jsonFlag = true
|
||||
case a == "--event":
|
||||
i++
|
||||
if i >= len(argv) {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument --event: expected one argument\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
event, hasEvent = argv[i], true
|
||||
case strings.HasPrefix(a, "--event="):
|
||||
event, hasEvent = strings.TrimPrefix(a, "--event="), true
|
||||
case a == "--data":
|
||||
i++
|
||||
if i >= len(argv) {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument --data: expected one argument\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
dataStr, hasData = argv[i], true
|
||||
case strings.HasPrefix(a, "--data="):
|
||||
dataStr, hasData = strings.TrimPrefix(a, "--data="), true
|
||||
case a == "-h" || a == "--help":
|
||||
argparseUsage(stdout)
|
||||
return 0
|
||||
case strings.HasPrefix(a, "-") && a != "-":
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: unrecognized arguments: %s\n", a)
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
default:
|
||||
positionals = append(positionals, a)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if len(positionals) == 0 {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: the following arguments are required: cmd\n")
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
if len(positionals) > 2 {
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: unrecognized arguments: %s\n", strings.Join(positionals[2:], " "))
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
cmd, arg := positionals[0], ""
|
||||
if len(positionals) == 2 {
|
||||
arg = positionals[1]
|
||||
}
|
||||
switch cmd {
|
||||
case "build":
|
||||
message := arg
|
||||
if message == "" {
|
||||
message = "general context"
|
||||
}
|
||||
context := buildAutoContext(message)
|
||||
if err := saveContext(context); err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if jsonFlag {
|
||||
fmt.Fprintln(stdout, encodePythonJSON(context, 0))
|
||||
} else {
|
||||
n := 0
|
||||
if warm := context.Get2("warm_neurons"); pyTruthy(warm) {
|
||||
if om, ok := warm.(*OrderedMap); ok {
|
||||
fired, _ := om.Get("fired")
|
||||
if fired == nil {
|
||||
if _, present := om.Get("fired"); !present {
|
||||
fired = []any{}
|
||||
}
|
||||
}
|
||||
if c, err := pyLen(fired); err == nil {
|
||||
n = c
|
||||
}
|
||||
}
|
||||
}
|
||||
m := 0
|
||||
if recall, ok := context.Get2("memory_recall").([]any); ok {
|
||||
m = len(recall)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Auto-context built: %d neurons, %d memories\n", n, m)
|
||||
}
|
||||
|
||||
case "show":
|
||||
raw, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
fmt.Fprintln(stdout, "No auto-context file found")
|
||||
return 0
|
||||
}
|
||||
v, err := pyParse(string(raw))
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintln(stdout, encodePythonJSON(v, 0))
|
||||
|
||||
case "hook":
|
||||
event2 := "unknown"
|
||||
if hasEvent && event != "" {
|
||||
event2 = event
|
||||
}
|
||||
var data *OrderedMap
|
||||
if hasData && dataStr != "" {
|
||||
v, err := pyParse(dataStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if om, ok := v.(*OrderedMap); ok {
|
||||
data = om
|
||||
} else {
|
||||
// json.loads gave a non-object; data.get would raise
|
||||
// AttributeError outside handle_hook's try
|
||||
fmt.Fprintf(stderr, "Traceback (most recent call last): '%s' object has no attribute 'get'\n", pyTypeName(v))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
result := handleHook(event2, data)
|
||||
fmt.Fprintln(stdout, encodePythonJSONCompact(result))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(stderr, "auto_context.py: error: argument cmd: invalid choice: '%s' (choose from 'build', 'show', 'hook')\n", cmd)
|
||||
argparseUsage(stderr)
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ── entry point: CLI + truth replay subcommand for the HARD CI gate ─────
|
||||
|
||||
func truthRowCount(path string) int {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return -1
|
||||
}
|
||||
return len(doc.Rows)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) >= 2 && os.Args[1] == "replay" {
|
||||
path := "truth.json"
|
||||
if len(os.Args) >= 3 {
|
||||
path = os.Args[2]
|
||||
}
|
||||
fails := RunTruth(path)
|
||||
if len(fails) > 0 {
|
||||
for _, f := range fails {
|
||||
fmt.Fprintln(os.Stderr, f)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "PARITY FAILED: %d divergence(s)\n", len(fails))
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("PARITY OK: %d truth rows exact-equal vs the Python oracle\n", truthRowCount(path))
|
||||
return
|
||||
}
|
||||
os.Exit(RunCLI(os.Args, os.Stdout))
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
// Python json interop: insertion-ordered maps and json.dumps-compatible
|
||||
// encoding (indent=2 and the default compact (", ", ": ") separators,
|
||||
// ensure_ascii=True) so save_context, show and hook CLI output are
|
||||
// byte-parity with the oracle (adapted from the message_queue port's
|
||||
// pyjson.go; the float renderer follows Python repr, which keeps ".0" on
|
||||
// integral floats).
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// KV — one ordered key/value pair.
|
||||
type KV struct {
|
||||
Key string
|
||||
Value any
|
||||
}
|
||||
|
||||
// OrderedMap — a dict with preserved insertion order.
|
||||
type OrderedMap struct {
|
||||
Keys []string
|
||||
Vals map[string]any
|
||||
}
|
||||
|
||||
// NewOrderedMap builds an ordered map from pairs.
|
||||
func NewOrderedMap(pairs ...KV) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for _, kv := range pairs {
|
||||
om.Set(kv.Key, kv.Value)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// OrderedMapFromMap wraps a plain map (order unspecified — canonical
|
||||
// comparisons sort keys anyway).
|
||||
func OrderedMapFromMap(m map[string]any) *OrderedMap {
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for k, v := range m {
|
||||
om.Set(k, v)
|
||||
}
|
||||
return om
|
||||
}
|
||||
|
||||
// Set inserts or replaces, preserving first-insertion order.
|
||||
func (o *OrderedMap) Set(key string, value any) {
|
||||
if _, ok := o.Vals[key]; !ok {
|
||||
o.Keys = append(o.Keys, key)
|
||||
}
|
||||
o.Vals[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a value.
|
||||
func (o *OrderedMap) Get(key string) (any, bool) {
|
||||
v, ok := o.Vals[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Get2 returns the value at key (nil when absent) for direct type asserts.
|
||||
func (o *OrderedMap) Get2(key string) any {
|
||||
return o.Vals[key]
|
||||
}
|
||||
|
||||
// Entries returns key/value pairs in insertion order.
|
||||
func (o *OrderedMap) Entries() []KV {
|
||||
out := make([]KV, 0, len(o.Keys))
|
||||
for _, k := range o.Keys {
|
||||
out = append(out, KV{k, o.Vals[k]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Len returns the number of entries.
|
||||
func (o *OrderedMap) Len() int { return len(o.Keys) }
|
||||
|
||||
// decodeJSONValue decodes one JSON value from the decoder, decoding objects
|
||||
// as *OrderedMap (order-preserving) and numbers as float64.
|
||||
func decodeJSONValue(dec *json.Decoder) (any, error) {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decodeFromToken(dec, tok)
|
||||
}
|
||||
|
||||
func decodeFromToken(dec *json.Decoder, tok json.Token) (any, error) {
|
||||
switch t := tok.(type) {
|
||||
case json.Delim:
|
||||
switch t {
|
||||
case '{':
|
||||
om := &OrderedMap{Vals: map[string]any{}}
|
||||
for dec.More() {
|
||||
keyTok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _ := keyTok.(string)
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
om.Set(key, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume '}'
|
||||
return nil, err
|
||||
}
|
||||
return om, nil
|
||||
case '[':
|
||||
arr := []any{}
|
||||
for dec.More() {
|
||||
val, err := decodeJSONValue(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
arr = append(arr, val)
|
||||
}
|
||||
if _, err := dec.Token(); err != nil { // consume ']'
|
||||
return nil, err
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected delim %v", t)
|
||||
default:
|
||||
return tok, nil
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSON renders v exactly like Python json.dumps(v, indent=N)
|
||||
// with default ensure_ascii=True and (',', ': ') separators.
|
||||
func encodePythonJSON(v any, indent int) string {
|
||||
var sb strings.Builder
|
||||
encodeValue(&sb, v, indent)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeValue(sb *strings.Builder, v any, indent int) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
if len(t) == 0 {
|
||||
sb.WriteString("[]")
|
||||
return
|
||||
}
|
||||
sb.WriteString("[\n")
|
||||
for i, item := range t {
|
||||
writeIndent(sb, indent+1)
|
||||
encodeValue(sb, item, indent+1)
|
||||
if i < len(t)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
if t.Len() == 0 {
|
||||
sb.WriteString("{}")
|
||||
return
|
||||
}
|
||||
sb.WriteString("{\n")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
writeIndent(sb, indent+1)
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeValue(sb, kv.Value, indent+1)
|
||||
if i < len(entries)-1 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
writeIndent(sb, indent)
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeValue(sb, OrderedMapFromMap(t), indent)
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
func writeIndent(sb *strings.Builder, level int) {
|
||||
for i := 0; i < level; i++ {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
// encodePythonJSONCompact renders v exactly like Python json.dumps(v) with
|
||||
// default separators (", ", ": ") and ensure_ascii=True — the compact form
|
||||
// the CLI hook path prints.
|
||||
func encodePythonJSONCompact(v any) string {
|
||||
var sb strings.Builder
|
||||
encodeCompactValue(&sb, v)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func encodeCompactValue(sb *strings.Builder, v any) {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if t {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pythonEscapeString(t))
|
||||
case int:
|
||||
sb.WriteString(strconv.Itoa(t))
|
||||
case int64:
|
||||
sb.WriteString(strconv.FormatInt(t, 10))
|
||||
case float64:
|
||||
sb.WriteString(pythonFloat(t))
|
||||
case json.Number:
|
||||
sb.WriteString(t.String())
|
||||
case []any:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []*OrderedMap:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case []string:
|
||||
sb.WriteString("[")
|
||||
for i, item := range t {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
encodeCompactValue(sb, item)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case *OrderedMap:
|
||||
sb.WriteString("{")
|
||||
entries := t.Entries()
|
||||
for i, kv := range entries {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pythonEscapeString(kv.Key))
|
||||
sb.WriteString(": ")
|
||||
encodeCompactValue(sb, kv.Value)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
case map[string]any:
|
||||
encodeCompactValue(sb, OrderedMapFromMap(t))
|
||||
default:
|
||||
sb.WriteString(pythonEscapeString(fmt.Sprintf("%v", t)))
|
||||
}
|
||||
}
|
||||
|
||||
// pythonFloat renders a float like Python repr (json.dumps uses
|
||||
// float.__repr__): shortest round-trip digits, and integral floats keep
|
||||
// one decimal place (1.0 stays "1.0", not "1").
|
||||
func pythonFloat(f float64) string {
|
||||
s := strconv.FormatFloat(f, 'g', -1, 64)
|
||||
if !strings.ContainsAny(s, ".eE") &&
|
||||
!strings.Contains(s, "Inf") && !strings.Contains(s, "NaN") {
|
||||
s += ".0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// pythonEscapeString — Python json.dumps string escaping with
|
||||
// ensure_ascii=True: control chars via \uXXXX (with \b \f \n \r \t
|
||||
// shortcuts), non-ASCII via \uXXXX / surrogate pairs, lowercase hex.
|
||||
func pythonEscapeString(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
sb.WriteString(`\"`)
|
||||
case '\\':
|
||||
sb.WriteString(`\\`)
|
||||
case '\n':
|
||||
sb.WriteString(`\n`)
|
||||
case '\r':
|
||||
sb.WriteString(`\r`)
|
||||
case '\t':
|
||||
sb.WriteString(`\t`)
|
||||
case '\b':
|
||||
sb.WriteString(`\b`)
|
||||
case '\f':
|
||||
sb.WriteString(`\f`)
|
||||
default:
|
||||
if r < 0x20 || r > 0x7e {
|
||||
if r > 0xffff {
|
||||
r1, r2 := utf16.EncodeRune(r)
|
||||
fmt.Fprintf(&sb, `\u%04x\u%04x`, r1, r2)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, `\u%04x`, r)
|
||||
}
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
// Replay — the frozen-truth parity engine. Reads truth.json (generated by
|
||||
// /tmp/gen_auto_context_truth.py against the live Python oracle with the
|
||||
// clock frozen and env-driven stub scripts pinned) and re-executes every
|
||||
// row through this Go port, comparing results. Exact rows (context-file
|
||||
// bytes, CLI stdout) compare byte-for-byte; everything else compares
|
||||
// canonically (sorted-key JSON). Absolute memory-dir paths are normalized
|
||||
// to @MEMDIR@ on both sides.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TruthDoc — the frozen truth file structure.
|
||||
type TruthDoc struct {
|
||||
FrozenTS string `json:"frozen_ts"`
|
||||
Oracle string `json:"oracle"`
|
||||
Rows []TruthRow `json:"rows"`
|
||||
}
|
||||
|
||||
// TruthRow — one frozen case.
|
||||
type TruthRow struct {
|
||||
Name string `json:"name"`
|
||||
Op string `json:"op"`
|
||||
Setup json.RawMessage `json:"setup"`
|
||||
Want json.RawMessage `json:"want"`
|
||||
}
|
||||
|
||||
// RunTruth replays every row and returns a divergence report (empty on 100%).
|
||||
func RunTruth(path string) []string {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("read truth: %v", err)}
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return []string{fmt.Sprintf("parse truth: %v", err)}
|
||||
}
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(&doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
return fails
|
||||
}
|
||||
|
||||
// compareOne replays one row and returns "" on parity, else a divergence
|
||||
// description. Shared by RunTruth and the per-function battery tests.
|
||||
func compareOne(doc *TruthDoc, row *TruthRow) string {
|
||||
got, exact, err := execRow(doc, row)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("error: %v", err)
|
||||
}
|
||||
var wantStr, gotStr string
|
||||
if exact {
|
||||
var w string
|
||||
if err := json.Unmarshal(row.Want, &w); err != nil {
|
||||
return fmt.Sprintf("want not a string: %v", err)
|
||||
}
|
||||
wantStr, gotStr = w, got.(string)
|
||||
} else {
|
||||
wantStr, err = canonicalFromRaw(row.Want)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize want: %v", err)
|
||||
}
|
||||
gotStr, err = canonicalFromRaw([]byte(mustJSON(got)))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("canonicalize got: %v", err)
|
||||
}
|
||||
}
|
||||
if wantStr != gotStr {
|
||||
return fmt.Sprintf("\n want: %s\n got: %s", wantStr, gotStr)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// execRow executes one truth row: returns got (any) and, for exact rows,
|
||||
// the exact string in got.
|
||||
func execRow(doc *TruthDoc, row *TruthRow) (got any, exact bool, err error) {
|
||||
setup := NewOrderedMap()
|
||||
if len(row.Setup) > 0 {
|
||||
dec := json.NewDecoder(bytes.NewReader(row.Setup))
|
||||
dec.UseNumber()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return nil, false, fmt.Errorf("setup not an object")
|
||||
}
|
||||
v, err := decodeFromToken(dec, tok)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
setup = v.(*OrderedMap)
|
||||
}
|
||||
|
||||
// freeze the seams per row: fresh temp memory dir, frozen clock,
|
||||
// env-driven fake runner mirroring /tmp/actx_stubs payloads
|
||||
home, err := os.MkdirTemp("", "actx_truth_row_")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer os.RemoveAll(home)
|
||||
|
||||
defer func() {
|
||||
MemoryDir = defaultMemoryDir
|
||||
ClockNow = defaultClockNow
|
||||
RunScript = defaultRunScript
|
||||
PythonBin = defaultPythonBin
|
||||
ScriptsDir = defaultScriptsDir
|
||||
}()
|
||||
memdir := filepath.Join(home, "memory")
|
||||
if err := os.MkdirAll(memdir, 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
MemoryDir = func() string { return memdir }
|
||||
ScriptsDir = func() string { return "/tmp/actx_stubs" }
|
||||
PythonBin = func() string { return "python3" }
|
||||
base, err := time.Parse(time.RFC3339, doc.FrozenTS)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
ClockNow = func() time.Time { return base }
|
||||
|
||||
// file seeds from setup
|
||||
if f, ok := setup.Get("tasks_file"); ok {
|
||||
s, _ := f.(string)
|
||||
if err := os.MkdirAll(filepath.Join(memdir, "tasks"), 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memdir, "tasks", "persisted.json"), []byte(s), 0o644); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
if f, ok := setup.Get("seed_context"); ok {
|
||||
s, _ := f.(string)
|
||||
if err := os.MkdirAll(filepath.Join(memdir, "index"), 0o777); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(memdir, "index", "auto_context.json"), []byte(s), 0o644); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// stub modes (default "ok", like the stub scripts' env defaults)
|
||||
mode := func(key string) string {
|
||||
if v, ok := setup.Get(key); ok {
|
||||
if s, ok2 := v.(string); ok2 {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
routeMode, alwaysMode, recallMode := mode("stub_route"), mode("stub_always"), mode("stub_recall")
|
||||
|
||||
RunScript = func(exe string, args []string, timeout time.Duration) ExecResult {
|
||||
var out string
|
||||
code := 0
|
||||
base := filepath.Base(args[0])
|
||||
if base == "warm_router.py" {
|
||||
if len(args) > 1 && args[1] == "always" {
|
||||
switch alwaysMode {
|
||||
case "ok":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"digest", "daily-summary"}, KV{"items", 7},
|
||||
KV{"topics", []any{"memory", "tasks", "recall"}}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "not-json{{{"
|
||||
case "empty":
|
||||
// print nothing
|
||||
}
|
||||
} else {
|
||||
msg := ""
|
||||
if len(args) > 2 {
|
||||
msg = args[2]
|
||||
}
|
||||
switch routeMode {
|
||||
case "ok":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"echo_message", msg},
|
||||
KV{"fired", []any{"warm-alpha", "warm-beta", "warm-gamma"}},
|
||||
KV{"matched", 3}, KV{"route", "focus"}))
|
||||
case "list":
|
||||
out = "[1, 2]"
|
||||
case "emptylist":
|
||||
out = "[]"
|
||||
case "firedint":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"fired", 3}, KV{"echo_message", msg}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "not-json{{{"
|
||||
case "empty":
|
||||
// print nothing
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := ""
|
||||
if len(args) > 2 {
|
||||
query = args[2]
|
||||
}
|
||||
switch recallMode {
|
||||
case "ok":
|
||||
items := []any{NewOrderedMap(KV{"echo_query", query})}
|
||||
for _, e := range recallEntries {
|
||||
items = append(items, e)
|
||||
}
|
||||
out = encodePythonJSONCompact(items)
|
||||
case "nonlist":
|
||||
out = encodePythonJSONCompact(NewOrderedMap(
|
||||
KV{"query", query}, KV{"note", "not a list"}))
|
||||
case "fail":
|
||||
code = 1
|
||||
case "badjson":
|
||||
out = "<xml?>"
|
||||
case "emptyout":
|
||||
// print nothing
|
||||
}
|
||||
}
|
||||
return ExecResult{Stdout: out, ReturnCode: code}
|
||||
}
|
||||
|
||||
scrub := func(v any) any {
|
||||
return scrubPaths(v, memdir)
|
||||
}
|
||||
|
||||
strSetup := func(key string) string {
|
||||
s, _ := setup.Get2(key).(string)
|
||||
return s
|
||||
}
|
||||
|
||||
switch row.Op {
|
||||
case "warm_router":
|
||||
return scrub(runWarmRouter(strSetup("message"))), false, nil
|
||||
|
||||
case "recall":
|
||||
budget := 5
|
||||
if b, ok := setup.Get("budget"); ok {
|
||||
switch n := b.(type) {
|
||||
case float64:
|
||||
budget = int(n)
|
||||
case json.Number:
|
||||
f, _ := n.Float64()
|
||||
budget = int(f)
|
||||
}
|
||||
}
|
||||
return scrub(runMemoryRecall(strSetup("query"), budget)), false, nil
|
||||
|
||||
case "always":
|
||||
return scrub(runAlwaysDigest()), false, nil
|
||||
|
||||
case "load_tasks":
|
||||
return scrub(loadActiveTasks()), false, nil
|
||||
|
||||
case "build":
|
||||
return scrub(buildAutoContext(strSetup("message"))), false, nil
|
||||
|
||||
case "save":
|
||||
om, ok := setup.Get2("context").(*OrderedMap)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("save: no context")
|
||||
}
|
||||
if err := saveContext(om); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reads := []any{scrubPaths(string(raw), memdir)}
|
||||
if om2, ok2 := setup.Get2("context2").(*OrderedMap); ok2 {
|
||||
// overwrite semantics: second save replaces the first atomically
|
||||
if err := saveContext(om2); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
raw2, err := os.ReadFile(contextFilePath())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
reads = append(reads, scrubPaths(string(raw2), memdir))
|
||||
}
|
||||
_, statErr := os.Stat(contextFilePath() + ".tmp")
|
||||
return NewOrderedMap(
|
||||
KV{"reads", reads},
|
||||
KV{"tmp_gone", os.IsNotExist(statErr)},
|
||||
), false, nil
|
||||
|
||||
case "hook":
|
||||
event := strSetup("event")
|
||||
var data *OrderedMap
|
||||
if v, ok := setup.Get("data"); ok && v != nil {
|
||||
om, ok2 := v.(*OrderedMap)
|
||||
if !ok2 {
|
||||
return nil, false, fmt.Errorf("hook: data not an object")
|
||||
}
|
||||
data = om
|
||||
}
|
||||
return scrub(handleHook(event, data)), false, nil
|
||||
|
||||
case "cli":
|
||||
argvAny, _ := setup.Get2("argv").([]any)
|
||||
argv := []string{}
|
||||
for _, a := range argvAny {
|
||||
s, _ := a.(string)
|
||||
argv = append(argv, s)
|
||||
}
|
||||
var sb strings.Builder
|
||||
code := RunCLI(argv, &sb)
|
||||
out := strings.ReplaceAll(sb.String(), memdir, "@MEMDIR@")
|
||||
return NewOrderedMap(KV{"stdout", out}, KV{"exit", code}), false, nil
|
||||
|
||||
case "plugin":
|
||||
return pluginManifest(), false, nil
|
||||
}
|
||||
return nil, false, fmt.Errorf("unknown row op %s (%s)", row.Op, row.Name)
|
||||
}
|
||||
|
||||
// scrubPaths — normalize the row's memory-dir prefix to @MEMDIR@ so the
|
||||
// frozen want (recorded with the generator's temp dirs) compares equal.
|
||||
func scrubPaths(v any, memdir string) any {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.ReplaceAll(t, memdir, "@MEMDIR@")
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = scrubPaths(item, memdir)
|
||||
}
|
||||
return out
|
||||
case *OrderedMap:
|
||||
out := NewOrderedMap()
|
||||
for _, kv := range t.Entries() {
|
||||
out.Set(kv.Key, scrubPaths(kv.Value, memdir))
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// recallEntries — the memory_store.py stub's fixed entry list (m2's title
|
||||
// pins ensure_ascii round-tripping).
|
||||
var recallEntries = []any{
|
||||
NewOrderedMap(KV{"id", "m1"}, KV{"score", 0.91},
|
||||
KV{"tags", []any{"alpha", "beta"}}, KV{"title", "memory-entry-one"}),
|
||||
NewOrderedMap(KV{"id", "m2"}, KV{"score", 0.84},
|
||||
KV{"tags", []any{}}, KV{"title", "mémoire—café"}),
|
||||
NewOrderedMap(KV{"id", "m3"}, KV{"score", 0.77},
|
||||
KV{"tags", []any{"gamma"}}, KV{"title", "memory-entry-three"}),
|
||||
NewOrderedMap(KV{"id", "m4"}, KV{"score", 0.7},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-four"}),
|
||||
NewOrderedMap(KV{"id", "m5"}, KV{"score", 0.62},
|
||||
KV{"tags", []any{"delta"}}, KV{"title", "memory-entry-five"}),
|
||||
NewOrderedMap(KV{"id", "m6"}, KV{"score", 0.55},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-six"}),
|
||||
NewOrderedMap(KV{"id", "m7"}, KV{"score", 0.41},
|
||||
KV{"tags", []any{"epsilon"}}, KV{"title", "memory-entry-seven"}),
|
||||
NewOrderedMap(KV{"id", "m8"}, KV{"score", 0.2},
|
||||
KV{"tags", []any{}}, KV{"title", "memory-entry-eight"}),
|
||||
}
|
||||
|
||||
// canonicalFromRaw — canonical string of a raw JSON document.
|
||||
func canonicalFromRaw(raw []byte) (string, error) {
|
||||
var v any
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return canonical(normalize(v)), nil
|
||||
}
|
||||
|
||||
// canonical — sorted-key deterministic rendering of a decoded JSON value.
|
||||
func canonical(v any) string {
|
||||
switch t := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case bool:
|
||||
if t {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case string:
|
||||
return pythonEscapeString(t)
|
||||
case float64:
|
||||
if t == float64(int64(t)) {
|
||||
return fmt.Sprintf("%d", int64(t))
|
||||
}
|
||||
return fmt.Sprintf("%v", t)
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return canonical(f)
|
||||
case []any:
|
||||
parts := make([]string, len(t))
|
||||
for i, item := range t {
|
||||
parts[i] = canonical(normalize(item))
|
||||
}
|
||||
return "[" + strings.Join(parts, ", ") + "]"
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(t))
|
||||
for k := range t {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
parts = append(parts, pythonEscapeString(k)+": "+canonical(normalize(t[k])))
|
||||
}
|
||||
return "{" + strings.Join(parts, ", ") + "}"
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = kv.Value
|
||||
}
|
||||
return canonical(m)
|
||||
default:
|
||||
return fmt.Sprintf("«%v»", t)
|
||||
}
|
||||
}
|
||||
|
||||
// normalize — convert OrderedMap/map trees into plain map[string]any with
|
||||
// json.Number→float64 so canonical() sees one representation.
|
||||
func normalize(v any) any {
|
||||
switch t := v.(type) {
|
||||
case *OrderedMap:
|
||||
m := map[string]any{}
|
||||
for _, kv := range t.Entries() {
|
||||
m[kv.Key] = normalize(kv.Value)
|
||||
}
|
||||
return m
|
||||
case map[string]any:
|
||||
m := map[string]any{}
|
||||
for k, val := range t {
|
||||
m[k] = normalize(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(t))
|
||||
for i, item := range t {
|
||||
out[i] = normalize(item)
|
||||
}
|
||||
return out
|
||||
case json.Number:
|
||||
f, _ := t.Float64()
|
||||
return f
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
b, err := json.Marshal(normalize(v))
|
||||
if err != nil {
|
||||
return "null"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,264 @@
|
||||
package main
|
||||
|
||||
// Battery — the HARD parity gate plus per-function batteries replaying the
|
||||
// frozen oracle truth (truth.json, generated against the LIVE Python
|
||||
// oracle with the clock frozen and env-driven stub scripts pinned)
|
||||
// VERBATIM. Any divergence fails; the truth file rides the repo so the
|
||||
// gate holds in CI without /tmp.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadTruth(t *testing.T) *TruthDoc {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("truth.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth.json missing (frozen oracle battery): %v", err)
|
||||
}
|
||||
var doc TruthDoc
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
t.Fatalf("parse truth.json: %v", err)
|
||||
}
|
||||
if len(doc.Rows) == 0 {
|
||||
t.Fatal("truth.json contained no rows")
|
||||
}
|
||||
return &doc
|
||||
}
|
||||
|
||||
func findRow(t *testing.T, name string) TruthRow {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
for _, row := range doc.Rows {
|
||||
if row.Name == name {
|
||||
return row
|
||||
}
|
||||
}
|
||||
t.Fatalf("frozen row %q missing from truth.json — battery/truth drift", name)
|
||||
return TruthRow{}
|
||||
}
|
||||
|
||||
func mustReplay(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
row := findRow(t, name)
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", name, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func replayOp(t *testing.T, op string) int {
|
||||
t.Helper()
|
||||
doc := loadTruth(t)
|
||||
n := 0
|
||||
for _, row := range doc.Rows {
|
||||
if row.Op != op {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
t.Fatalf("DIVERGED %s: %s", row.Name, msg)
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatalf("no %s rows in truth.json", op)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// rowWant decodes a row's want JSON into a generic map.
|
||||
func rowWant(t *testing.T, name string) map[string]any {
|
||||
t.Helper()
|
||||
row := findRow(t, name)
|
||||
var want map[string]any
|
||||
if err := json.Unmarshal(row.Want, &want); err != nil {
|
||||
t.Fatalf("decode want of %s: %v", name, err)
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// TestTruthParity — the whole frozen battery, one shot.
|
||||
func TestTruthParity(t *testing.T) {
|
||||
doc := loadTruth(t)
|
||||
var fails []string
|
||||
for _, row := range doc.Rows {
|
||||
if msg := compareOne(doc, &row); msg != "" {
|
||||
fails = append(fails, row.Name+": "+msg)
|
||||
}
|
||||
}
|
||||
if len(fails) > 0 {
|
||||
t.Fatalf("parity replay diverged after %d clean rows:\n%s",
|
||||
len(doc.Rows)-len(fails), strings.Join(fails, "\n"))
|
||||
}
|
||||
t.Logf("PARITY OK: %d cases exact-equal vs the Python oracle", len(doc.Rows))
|
||||
}
|
||||
|
||||
// TestBatteryRunWarmRouter — ok / nonzero-exit / bad-json / empty-stdout /
|
||||
// JSON-list payload / empty-list payload; the ok row pins the exact
|
||||
// message the port hands to the seam.
|
||||
func TestBatteryRunWarmRouter(t *testing.T) {
|
||||
if n := replayOp(t, "warm_router"); n != 6 {
|
||||
t.Fatalf("expected 6 frozen warm_router rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "warm:ok")
|
||||
if want["echo_message"] != "hello warm router" || want["matched"] != float64(3) {
|
||||
t.Fatalf("frozen echo row changed: %v", want)
|
||||
}
|
||||
if got := len(want["fired"].([]any)); got != 3 {
|
||||
t.Fatalf("fired payload changed: %v", want["fired"])
|
||||
}
|
||||
mustReplay(t, "warm:list")
|
||||
mustReplay(t, "warm:emptylist")
|
||||
}
|
||||
|
||||
// TestBatteryRunMemoryRecall — ok (echo pins the query string), budget
|
||||
// truncation, non-list payload, and every degrade branch.
|
||||
func TestBatteryRunMemoryRecall(t *testing.T) {
|
||||
if n := replayOp(t, "recall"); n != 6 {
|
||||
t.Fatalf("expected 6 frozen recall rows, got %d", n)
|
||||
}
|
||||
// recall:ok's want is the returned LIST — element 0 echoes the query
|
||||
rawWant := findRow(t, "recall:ok").Want
|
||||
var list []map[string]any
|
||||
if err := json.Unmarshal(rawWant, &list); err != nil {
|
||||
t.Fatalf("decode recall:ok want: %v", err)
|
||||
}
|
||||
if len(list) != 5 || list[0]["echo_query"] != "alpha beta gamma" {
|
||||
t.Fatalf("frozen recall:ok changed: %v", list)
|
||||
}
|
||||
mustReplay(t, "recall:budget2")
|
||||
mustReplay(t, "recall:nonlist")
|
||||
mustReplay(t, "recall:emptyout")
|
||||
}
|
||||
|
||||
// TestBatteryRunAlwaysDigest — ok / fail / bad-json / empty.
|
||||
func TestBatteryRunAlwaysDigest(t *testing.T) {
|
||||
if n := replayOp(t, "always"); n != 4 {
|
||||
t.Fatalf("expected 4 frozen always rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "always:ok")
|
||||
if want["digest"] != "daily-summary" || want["items"] != float64(7) {
|
||||
t.Fatalf("frozen digest changed: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryLoadActiveTasks — missing file / mixed statuses (only
|
||||
// open+in_progress survive) / malformed / empty / no-tasks-key.
|
||||
func TestBatteryLoadActiveTasks(t *testing.T) {
|
||||
if n := replayOp(t, "load_tasks"); n != 5 {
|
||||
t.Fatalf("expected 5 frozen load_tasks rows, got %d", n)
|
||||
}
|
||||
mustReplay(t, "tasks:missing")
|
||||
mustReplay(t, "tasks:malformed")
|
||||
}
|
||||
|
||||
// TestBatteryBuild — every build_auto_context branch: full context (the
|
||||
// recall echo pins stopword filtering + isalpha + first-5 join), short
|
||||
// message, stopwords-only, non-alpha, unicode 200-rune preview truncation,
|
||||
// empty message, recall-fail, warm-emptylist, warm-fail; frozen timestamp.
|
||||
func TestBatteryBuild(t *testing.T) {
|
||||
if n := replayOp(t, "build"); n != 9 {
|
||||
t.Fatalf("expected 9 frozen build rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "build:full")
|
||||
recallList, _ := want["memory_recall"].([]any)
|
||||
echo, _ := recallList[0].(map[string]any)
|
||||
if echo["echo_query"] != "remember config parser override discussion" {
|
||||
t.Fatalf("recall query extraction changed: %v", echo["echo_query"])
|
||||
}
|
||||
if want["timestamp"] != "2026-10-11T12:00:00.123456+00:00" {
|
||||
t.Fatalf("frozen timestamp changed: %v", want["timestamp"])
|
||||
}
|
||||
want = rowWant(t, "build:unicode-preview")
|
||||
preview, _ := want["message_preview"].(string)
|
||||
if n := len([]rune(preview)); n != 200 {
|
||||
t.Fatalf("preview must be 200 runes, got %d", n)
|
||||
}
|
||||
mustReplay(t, "build:short-message")
|
||||
mustReplay(t, "build:stopwords")
|
||||
mustReplay(t, "build:nonalpha")
|
||||
mustReplay(t, "build:empty")
|
||||
mustReplay(t, "build:recall-fail")
|
||||
mustReplay(t, "build:warm-emptylist")
|
||||
mustReplay(t, "build:warm-fail")
|
||||
}
|
||||
|
||||
// TestBatterySave — save_context byte-parity: ensure_ascii escapes,
|
||||
// indent=2 layout, and the tmp+rename overwrite (no .tmp leftover).
|
||||
func TestBatterySave(t *testing.T) {
|
||||
if n := replayOp(t, "save"); n != 2 {
|
||||
t.Fatalf("expected 2 frozen save rows, got %d", n)
|
||||
}
|
||||
mustReplay(t, "save:full")
|
||||
mustReplay(t, "save:overwrite")
|
||||
}
|
||||
|
||||
// TestBatteryHandleHook — full counts, text fallback, str(data) fallback
|
||||
// (Python dict repr!), short message, other event, no data, zero counts,
|
||||
// and both best-effort error dicts verbatim.
|
||||
func TestBatteryHandleHook(t *testing.T) {
|
||||
if n := replayOp(t, "hook"); n != 9 {
|
||||
t.Fatalf("expected 9 frozen hook rows, got %d", n)
|
||||
}
|
||||
want := rowWant(t, "hook:full")
|
||||
if want["neurons_fired"] != float64(3) || want["memories_recalled"] != float64(5) ||
|
||||
want["active_tasks"] != float64(2) {
|
||||
t.Fatalf("frozen hook counts changed: %v", want)
|
||||
}
|
||||
if cf, _ := want["context_file"].(string); !strings.HasSuffix(cf, "@MEMDIR@/index/auto_context.json") {
|
||||
t.Fatalf("context_file normalization changed: %v", cf)
|
||||
}
|
||||
errWant := rowWant(t, "hook:warm-list-error")
|
||||
if errWant["error"] != "'list' object has no attribute 'get'" {
|
||||
t.Fatalf("list.get error message changed: %v", errWant["error"])
|
||||
}
|
||||
errWant = rowWant(t, "hook:fired-int-error")
|
||||
if errWant["error"] != "object of type 'int' has no len()" {
|
||||
t.Fatalf("len(int) error message changed: %v", errWant["error"])
|
||||
}
|
||||
mustReplay(t, "hook:text-fallback")
|
||||
mustReplay(t, "hook:str-fallback")
|
||||
mustReplay(t, "hook:short-message")
|
||||
mustReplay(t, "hook:other-event")
|
||||
mustReplay(t, "hook:no-data")
|
||||
mustReplay(t, "hook:counts-zero")
|
||||
}
|
||||
|
||||
// TestBatteryCLI — main() byte-parity stdout + exit codes: build (plain +
|
||||
// --json + default message), show (hit + missing), hook (event+data /
|
||||
// defaults / error dict), argparse exits.
|
||||
func TestBatteryCLI(t *testing.T) {
|
||||
if n := replayOp(t, "cli"); n != 10 {
|
||||
t.Fatalf("expected 10 frozen cli rows, got %d", n)
|
||||
}
|
||||
for name, wantExit := range map[string]float64{
|
||||
"cli:build": 0,
|
||||
"cli:build-json": 0,
|
||||
"cli:build-default": 0,
|
||||
"cli:show-missing": 0,
|
||||
"cli:show-hit": 0,
|
||||
"cli:hook": 0,
|
||||
"cli:hook-default": 0,
|
||||
"cli:hook-error": 0,
|
||||
"cli:bad-cmd": 2,
|
||||
"cli:no-cmd": 2,
|
||||
} {
|
||||
want := rowWant(t, name)
|
||||
if want["exit"] != wantExit {
|
||||
t.Fatalf("%s: frozen exit code %v != %v", name, want["exit"], wantExit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatteryPlugin — the PLUGIN manifest row.
|
||||
func TestBatteryPlugin(t *testing.T) {
|
||||
mustReplay(t, "plugin")
|
||||
want := rowWant(t, "plugin")
|
||||
if want["name"] != "auto_context" || want["priority"] != float64(5) ||
|
||||
want["auto_activate"] != true {
|
||||
t.Fatalf("frozen PLUGIN manifest changed: %v", want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user