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
1 change: 1 addition & 0 deletions docs/memory/MEMORY.md
Original file line number Diff line number Diff line change
@@ -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=<stageDir>` 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.
49 changes: 49 additions & 0 deletions docs/memory/docker-routines-gtmroutines-fallback.md
Original file line number Diff line number Diff line change
@@ -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 <c> bash -lc 'export ydb_routines="<stageDir> $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="<stageDir> "` (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="<stageDir> ${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.
27 changes: 20 additions & 7 deletions internal/engine/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,33 @@ import (
// DockerRunner is a Runner transport that runs argv inside a running container
// via `docker exec -i <container> 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 {
Expand Down
30 changes: 30 additions & 0 deletions internal/engine/docker_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading