diff --git a/docs/memory/MEMORY.md b/docs/memory/MEMORY.md index 91075af..a77aaf2 100644 --- a/docs/memory/MEMORY.md +++ b/docs/memory/MEMORY.md @@ -1,4 +1,5 @@ # m-cli memory index +- [docker routines gtmroutines fallback](docker-routines-gtmroutines-fallback.md) — Fixed 2026-06-16: `m test --docker` (DockerEngine) set `ydb_routines=` only, which **overrides** a GT.M VistA's `gtmroutines` → vehu's resident routines (XPAR/FileMan) vanished → VistA-dependent suites faulted **0/0**. `dockerEnvPrefix` now falls back to `${ydb_routines:-$gtmroutines}`. **Validated: VSLCFGTST 0/0→3/3 GREEN on vehu (YDB) + foia-t12 (IRIS)**; m-test-engine regression green. Note the path split: `m test --docker` = m-cli DockerEngine (this fix); `m vista exec`/`v pkg` = m-ydb driver (M_YDB_* knobs). Unblocks all VistA-dependent VSL*TST. Branch `docker-routines-base`. - [chset byte mode](chset-byte-mode.md) — `--chset m|utf-8` on test/coverage/watch; m-stdlib byte suites need `m` on YDB - [arch waterline gates G2/G3/G4](arch-g2-forbidden-symbol.md) — `m arch check` gained the full suite: **G2** (no VistA symbols below the line; comment-aware deny-list, RE2 trailing-guard avoids `^DIETST`), **G3** (transport-monopoly — flags a non-SDK repo *exec*-ing `"m-ydb"/"m-iris"`; co-occurrence with `exec.Command` makes it self-hosting), **G4** (seam-pin — go.mod text-parse flags `replace`/pseudo-version of m-driver-sdk). G1/G2 = m-layer; G3/G4 = all repos. **Item 1 (meta-schema validation) also done:** root-first `metaCandidates`, `LoadMeta`/`ValidateMeta` require id/layer/language/verification_commands; `Gate:"META"`. Gotcha: consumes/exposes are objects not arrays → `Meta` struct holds only the 4 required fields. All 8 repos clean. Owed: migrate m-stdlib/v-stdlib off `dist/` to root meta; meta-gate; m-ci.yml; tag/pin. diff --git a/docs/memory/docker-routines-gtmroutines-fallback.md b/docs/memory/docker-routines-gtmroutines-fallback.md new file mode 100644 index 0000000..35855a5 --- /dev/null +++ b/docs/memory/docker-routines-gtmroutines-fallback.md @@ -0,0 +1,49 @@ +--- +name: docker-routines-gtmroutines-fallback +description: Fixed (2026-06-16) — `m test --docker` dropped a GT.M VistA's resident routines (set ydb_routines=stageDir-only, which overrides gtmroutines), so VistA-dependent suites faulted 0/0. DockerRunner now falls back to ${ydb_routines:-$gtmroutines}. Unblocks VSL T1.2 / all VistA-dependent VSL*TST. +metadata: + type: project +--- + +# `m test --docker` must layer the engine's resident routine base (the gtmroutines fallback) + +**Bug.** `internal/engine/docker.go` `DockerRunner` ran the suite via +`docker exec -i bash -lc 'export ydb_routines=" $ydb_routines"; …'`. +For the bare **m-test-engine** (a YDB image, sets `ydb_routines`) this is correct. +But a **GT.M-configured VistA** — the FOIA **`vehu`** image — sets **`gtmroutines`** +(GT.M name), NOT `ydb_routines`. So the ambient `$ydb_routines` is empty and the +export became `ydb_routines=" "` (staged dir only). GT.M V7.0 honors +`ydb_routines` over `gtmroutines` once it is set, so **vehu's resident VistA +routines (XPAR, FileMan, XLFDT, …) vanished from the path** — any VistA-dependent +suite faulted before `report^STDASSERT` and the runner showed **0/0**. + +Globals were unaffected: docker.go never sets `ydb_gbldir`, so the ambient +`gtmgbldir` survives — which is why the gbldir half worked and only routines broke. + +**Fix (`dockerEnvPrefix`, ~6 lines).** Prepend the staged dir, base falling back to +`$gtmroutines` when `$ydb_routines` is unset: +`export ydb_routines=" ${ydb_routines:-$gtmroutines}"; ` +- bare YDB (m-test-engine): `ydb_routines` set → fallback unused → unchanged. +- GT.M VistA (vehu): `ydb_routines` empty → base = `$gtmroutines` → staged AND + resident routines both resolve. +Extracted to the pure helper `dockerEnvPrefix(stageDir)` for a table test +(`docker_test.go`, TDD red→green). + +**This is the m-cli (DockerEngine) analog of the m-ydb `$ZGBLDIR` fix** +([[../../../m-ydb/docs/memory/m-ydb-docker-gbldir]]). NOTE the path split that made +this subtle: `m test --docker` uses **m-cli's internal DockerEngine** +(`internal/engine/docker.go` + `ydb.go`), whereas `m vista exec` / `v pkg` use the +**m-ydb driver** (`mdriver.Client`). The m-ydb gbldir/routines env knobs +(`M_YDB_GBLDIR`/`M_YDB_ROUTINES`) apply to the *driver* path, NOT to `m test`. +After this fix `m test --docker vehu` needs **no** `M_YDB_*` host vars — the +container's `bash -l` env supplies `gtmgbldir` + `gtmroutines`. + +**Validated (the acceptance gate).** v-stdlib `VSLCFGTST` (XPAR config adapter) — +`0/0` before → **3/3 GREEN after** on BOTH engines via the driver stack: +`m test --engine ydb --docker vehu --chset m …` and +`m test --engine iris --docker foia-t12 --namespace VISTA …`. IRIS needed no change +(routines are namespace-resident). m-test-engine regression green (2/2, fallback +unused). `make lint` + `go test ./...` clean. Branch `docker-routines-base`. + +**Why it matters:** unblocks **all** VistA-dependent `VSL*TST` testing over +`m test --docker`, not just VSLCFG — the M1 walking skeleton (T1.2→T1.5) and beyond. diff --git a/internal/engine/docker.go b/internal/engine/docker.go index 1edd5ad..d77a7d0 100644 --- a/internal/engine/docker.go +++ b/internal/engine/docker.go @@ -11,20 +11,33 @@ import ( // DockerRunner is a Runner transport that runs argv inside a running container // via `docker exec -i bash -lc …` — `bash -lc` loads the engine's // shell env (e.g. YottaDB's $ydb_dist/$ydb_routines). When stageDir is -// non-empty it is prepended to $ydb_routines so routines staged there resolve -// and auto-compile (the m-test-engine bind-mounts $HOME/m-work → /m-work, but -// /m-work is not on the default $ydb_routines). +// non-empty it is prepended to the routine path so routines staged there +// resolve and auto-compile (the m-test-engine bind-mounts $HOME/m-work → +// /m-work, but /m-work is not on the default $ydb_routines). func DockerRunner(container, stageDir string) Runner { return func(ctx context.Context, argv []string, stdin string) (Result, error) { - inner := shJoin(argv) - if stageDir != "" { - inner = `export ydb_routines="` + stageDir + ` $ydb_routines"; ` + inner - } + inner := dockerEnvPrefix(stageDir) + shJoin(argv) dargv := []string{"docker", "exec", "-i", container, "bash", "-lc", inner} return LocalRunner(ctx, dargv, stdin) } } +// dockerEnvPrefix is the `export …; ` shell prefix that puts the staged routine +// dir on the engine's routine search path, ahead of the engine's own resident +// routines. The base falls back to $gtmroutines when $ydb_routines is unset: a +// GT.M-configured VistA (e.g. the FOIA `vehu` image) sets gtmroutines, not +// ydb_routines — and once ydb_routines is set it OVERRIDES gtmroutines, so it +// must carry the resident base or the engine's own routines (XPAR, FileMan, …) +// disappear from the path and VistA-dependent suites fault. (gbldir needs no +// such handling: docker.go never sets ydb_gbldir, so the ambient gtmgbldir +// survives.) Empty stageDir → empty prefix (unchanged behavior). +func dockerEnvPrefix(stageDir string) string { + if stageDir == "" { + return "" + } + return `export ydb_routines="` + stageDir + ` ${ydb_routines:-$gtmroutines}"; ` +} + // DockerStage creates stageDir inside the container and copies files into it // (via `docker cp`, which works regardless of bind-mount ownership on the host). func DockerStage(ctx context.Context, container, stageDir string, files []string) error { diff --git a/internal/engine/docker_test.go b/internal/engine/docker_test.go new file mode 100644 index 0000000..b9109da --- /dev/null +++ b/internal/engine/docker_test.go @@ -0,0 +1,30 @@ +package engine + +import "testing" + +func TestDockerEnvPrefix(t *testing.T) { + cases := []struct { + name string + stageDir string + want string + }{ + {"no stage dir → no prefix", "", ""}, + { + // The staged dir is prepended to the routine path, and the base falls + // back to $gtmroutines when $ydb_routines is unset — a GT.M-configured + // VistA (e.g. vehu) sets gtmroutines, not ydb_routines, and ydb_routines + // once set overrides gtmroutines, so it must carry the resident base or + // the engine's own routines (XPAR, FileMan, …) vanish. + "stage dir prepends + falls back to gtmroutines", + "/m-work", + `export ydb_routines="/m-work ${ydb_routines:-$gtmroutines}"; `, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := dockerEnvPrefix(tc.stageDir); got != tc.want { + t.Errorf("dockerEnvPrefix(%q) = %q, want %q", tc.stageDir, got, tc.want) + } + }) + } +}