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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Phase-0 manifest contract gate
# Asserts dist/repo.meta.json parses, carries all required fields,
# and every exposes.* path resolves on disk. Cross-repo tier-2
# contract per .github/docs/AI-discoverability-plan.md §3.4.
# Runs before the Docker build so a broken manifest fails fast.
run: make check-manifest

- name: docs/ prose-only gate
# Cross-repo guardrail: docs/ holds only human-readable prose.
run: make check-docs-prose

- name: Build image
working-directory: docker
run: docker compose build
Expand Down
134 changes: 134 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
# Machine-readable project descriptor.
name: m-test-engine
kind: [infrastructure, container]
status: active
languages: [dockerfile]

distribution:
github: m-dev-tools/m-test-engine

location: ~/m-dev-tools/m-test-engine

exposes:
container_name: "m-test-engine"
image_base: "yottadb/yottadb-base:latest-master"
mount_point: "/work"
lifecycle: "make up | down | smoke | shell | clean | logs"

consumes:
formats: []
services: []

companions:
- project: m-cli
relation: "consumer — DockerEngine transport drives `docker exec m-test-engine` for `m test` / `m coverage`"
- project: m-stdlib
relation: "consumer — its CI uses this container's image base (yottadb-base:latest-master); test runner is engine-agnostic and works against this container too"

incompatibilities:
- "Not a VistA runtime. No FileMan, no RPC broker, no Kernel. Use vista-meta for VistA work."
- "No SSH server in the default container. m-cli's SSHEngine transport requires the optional compose.ssh.yml overlay (not shipped)."

docs:
primary: README.md
---

# m-test-engine — Claude project context

Minimal YottaDB Docker container for `m-cli` and `m-stdlib` testing.
Long-running container exposing a YottaDB engine via `docker exec`.
Consumer projects bind-mount their source as `/work` and dispatch
`docker exec m-test-engine $ydb_dist/mumps -run ...` commands.

The full design rationale and lifecycle table is in `README.md`.

## What this is

- A `Dockerfile` (atop `yottadb/yottadb-base:latest-master`) plus a
`compose.yml` plus thin Makefile wrappers (`up`, `down`, `smoke`,
`shell`, `clean`, `logs`).
- A container name (`m-test-engine`) and a bind-mount contract
(`/work` in container = consumer's `$PWD` on host).
- A `dist/lifecycle.json` describing those facts in
machine-readable form for the org-level AI-discoverability catalog.

## What this is NOT

- A test framework. The tests live in the consumer repos.
- A VistA system. No FileMan, no Kernel, no RPC. Use vista-meta.
- A long-lived data store. The globals volume
(`m-test-engine-globals`) persists between runs as a convenience
but is destructively cleared by `make clean`.

## Setup

```bash
git clone https://github.com/m-dev-tools/m-test-engine
```

Requires Docker + `docker compose`. No Python or Node.

## Test

```bash
make smoke # one-shot: `docker exec ... $ydb_dist/mumps -run %XCMD 'write "ok",!'`
```

The smoke test is also the CI gate — it asserts the container starts
clean on a fresh checkout, the YDB environment loads via
`/etc/profile.d/ydb-env.sh`, and `mumps -run %XCMD` produces the
expected output. If smoke fails, the container is unusable; consumer
projects (m-cli, m-stdlib) will fail too.

## Build / generate

```bash
make up # build + start the container detached
make down # stop + remove the container (globals volume preserved)
make logs # tail container stdout (debugging startup)
make shell # interactive bash inside the container
make clean # DESTRUCTIVE — stop + remove + drop globals volume
```

The `dist/lifecycle.json` published by this repo for AI-discoverability
is hand-authored. When the Dockerfile or `compose.yml` changes in a way
that affects an external claim (image base, container name, mount
point, exposed make targets), update `dist/lifecycle.json` in the same
commit.

## Verify

The `verification_commands` declared in `dist/repo.meta.json`:

```bash
make smoke # container starts + mumps runs
make check-manifest # dist/repo.meta.json valid + exposes.* paths exist
```

Plus the cross-repo guardrail:

```bash
make check-docs-prose # docs/ holds only prose (this repo has no docs/ at all)
```

## Guardrails

- **Do not add VistA-specific extras.** No FileMan, no RPC broker, no
Kernel routines, no Octo SQL. m-test-engine is deliberately minimal;
VistA work belongs in vista-meta.
- **Do not add an SSH server to the default image.** If a consumer
needs `m-cli`'s `SSHEngine` transport, ship an opt-in overlay
(`compose.ssh.yml`) rather than expanding the default container.
- **Do not hand-edit `dist/repo.meta.json` `verified_on` to a future
date.** The org smoke test rejects manifests older than 90 days; bump
the date only when the manifest changes materially (image bump,
container rename, mount-point change).
- **Container name and mount point are public contract.**
`m-test-engine` (name) and `/work` (mount) are referenced by
consumer code paths in m-cli and m-stdlib. Renaming either is a
breaking change requiring coordinated updates in those repos.
- **Image base is pinned for a reason.** The Dockerfile pin
(`yottadb-base:latest-master`) matches what `m-stdlib`'s CI uses.
Bumping requires confirming both consumers still pass against the
new base.
1 change: 1 addition & 0 deletions CLAUDE.md
41 changes: 40 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# But run-from-here works too: cd into m-test-engine and `make smoke`
# for a quick check.

.PHONY: up down logs shell smoke clean
.PHONY: up down logs shell smoke clean manifest check-manifest check-docs-prose

COMPOSE := docker compose -f docker/compose.yml

Expand Down Expand Up @@ -48,3 +48,42 @@ smoke:
# left ^STDLIB or similar in a bad state and you want a clean slate.
clean:
$(COMPOSE) down -v

# ── Phase-0 AI-discoverability contract ───────────────────────────────
#
# Tier-2 entry to the org catalog. See
# https://github.com/m-dev-tools/.github/blob/main/docs/AI-discoverability-plan.md
#
# `dist/lifecycle.json` is hand-authored, not regenerated — the source
# of truth for the container's name, image base, mount point, and the
# make/compose lifecycle targets is the Dockerfile + compose.yml + this
# Makefile, all already committed. When any of those change in a way
# that affects an external claim, update lifecycle.json in the same
# commit (this is captured in AGENTS.md § Guardrails).
#
# `make manifest` is therefore a pointer no-op — it exists so
# verification_commands in dist/repo.meta.json line up with other org
# repos.

manifest:
@echo "m-test-engine: dist/lifecycle.json is hand-authored alongside Dockerfile + compose.yml."
@echo " see AGENTS.md § Build / generate for the rebuild-when-it-changes guardrail."

check-manifest:
python3 tools/check-manifest.py

# Guardrail: docs/ holds only human-readable prose. Same target name
# as the tier-1 repos so cross-repo muscle memory works.
check-docs-prose:
@if [ ! -d docs ]; then echo "check-docs-prose: no docs/ directory ✓"; exit 0; fi; \
violations=$$(find docs -type f \
! -name '*.md' ! -name '*.markdown' \
! -name '*.png' ! -name '*.jpg' ! -name '*.jpeg' \
! -name '*.gif' ! -name '*.svg' ! -name '*.webp' \
! -name '.gitkeep'); \
if [ -n "$$violations" ]; then \
echo "ERROR: non-prose files under docs/ — move to a top-level domain dir:" >&2; \
echo "$$violations" >&2; \
exit 1; \
fi; \
echo "check-docs-prose: docs/ is prose-only ✓"
31 changes: 31 additions & 0 deletions dist/lifecycle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"schema_version": "1",
"container_name": "m-test-engine",
"hostname": "m-test-engine",
"image": "m-test-engine:latest",
"image_base": "yottadb/yottadb-base:latest-master",
"mount_point": "/work",
"mount_source_env": "M_TEST_ENGINE_BIND",
"mount_source_default": "$PWD",
"globals_volume": "m-test-engine-globals",
"ports": [],
"ssh_server": false,
"lifecycle": {
"up": { "make": "make up", "compose": "docker compose -f docker/compose.yml up -d --build" },
"down": { "make": "make down", "compose": "docker compose -f docker/compose.yml down" },
"logs": { "make": "make logs", "compose": "docker compose -f docker/compose.yml logs -f" },
"shell": { "make": "make shell", "compose": "docker exec -it m-test-engine bash" },
"smoke": { "make": "make smoke", "compose": "docker exec m-test-engine bash -lc '$ydb_dist/mumps -run %XCMD '\\''write \"smoke ok\",!'\\'''" },
"clean": { "make": "make clean", "compose": "docker compose -f docker/compose.yml down -v" }
},
"exec_convention": {
"shape": "docker exec m-test-engine bash -lc '<command>'",
"ydb_env_loaded_via": "/etc/profile.d/ydb-env.sh (sources /opt/yottadb/current/ydb_env_set)",
"available_env_vars": ["ydb_dist", "ydb_routines"],
"notes": "Use bash -lc so the YDB env loads. Single-quote M commands so the outer bash does not expand $ZVERSION etc. (those are YDB special variables, not shell vars)."
},
"consumers": [
{ "repo": "m-cli", "transport": "DockerEngine (src/m_cli/engine.py)" },
{ "repo": "m-stdlib", "transport": "test runner; reuses m-cli's DockerEngine when m-cli is installed" }
]
}
18 changes: 18 additions & 0 deletions dist/repo.meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://raw.githubusercontent.com/m-dev-tools/.github/main/profile/repo.meta.schema.json",
"id": "tool:m-test-engine",
"repo": "https://github.com/m-dev-tools/m-test-engine",
"role": "Minimal YottaDB Docker container — lightweight default test substrate for m-cli + m-stdlib",
"language": ["dockerfile"],
"license": "AGPL-3.0",
"agent_instructions": "AGENTS.md",
"verified_on": "2026-05-10",
"exposes": {
"lifecycle": "dist/lifecycle.json",
"dockerfile": "docker/Dockerfile",
"compose": "docker/compose.yml"
},
"verification_commands": ["make smoke", "make check-manifest"],
"status": "active",
"notes": "Container name 'm-test-engine' and mount point '/work' are public contract — m-cli's DockerEngine and m-stdlib's test runner reference them. Image base 'yottadb-base:latest-master' is pinned to match m-stdlib's CI."
}
108 changes: 108 additions & 0 deletions tools/check-manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Phase-0 contract gate for dist/repo.meta.json.

Validates that:
1. dist/repo.meta.json parses as JSON.
2. Required fields from the org-level repo.meta.schema.json contract
are present.
3. Each path under `exposes.*` resolves on disk.
4. (Best-effort) full schema validation if jsonschema is available
and the canonical schema URL is reachable.

Exits 0 on success; non-zero with structured stderr on failure.

Engine-free, no Node, no Python deps beyond the standard library
unless jsonschema happens to be installed.
"""

from __future__ import annotations

import json
import sys
import urllib.request
from pathlib import Path

MANIFEST = Path("dist/repo.meta.json")

REQUIRED_FIELDS = (
"id",
"repo",
"role",
"language",
"license",
"agent_instructions",
"verified_on",
"exposes",
"verification_commands",
)


def main() -> int:
if not MANIFEST.exists():
print(f"ERROR: {MANIFEST} not found", file=sys.stderr)
return 1

try:
data = json.loads(MANIFEST.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
print(f"ERROR: {MANIFEST} is invalid JSON: {exc}", file=sys.stderr)
return 1

missing = [f for f in REQUIRED_FIELDS if f not in data]
if missing:
print(f"ERROR: missing required fields: {missing}", file=sys.stderr)
return 1

fail = False
for key, rel_path in data["exposes"].items():
if rel_path.startswith(("http://", "https://")):
continue
if not Path(rel_path).exists():
print(
f"ERROR: exposes.{key} payload missing on disk: {rel_path}",
file=sys.stderr,
)
fail = True
if fail:
return 1

# Best-effort full schema validation. Skipped silently if jsonschema
# isn't available (the canonical Track-A validator runs in the org
# smoke test against the same manifest).
try:
from jsonschema import Draft202012Validator # type: ignore
except ImportError:
print(
"check-manifest: dist/repo.meta.json valid; "
"all exposes.* present ✓ (jsonschema not installed — "
"skipping full schema validation)"
)
return 0

schema_uri = data.get("$schema", "")
try:
with urllib.request.urlopen(schema_uri, timeout=5) as resp:
schema = json.load(resp)
except Exception as exc: # noqa: BLE001
print(
f"check-manifest: dist/repo.meta.json valid; all exposes.* "
f"present ✓ (skipped live schema fetch: {exc})"
)
return 0

errors = list(Draft202012Validator(schema).iter_errors(data))
if errors:
for err in errors:
path = "/".join(str(p) for p in err.absolute_path) or "<root>"
print(f"SCHEMA ERROR at {path}: {err.message}", file=sys.stderr)
return 1

print(
"check-manifest: dist/repo.meta.json valid against org schema; "
"all exposes.* present ✓"
)
return 0


if __name__ == "__main__":
sys.exit(main())
Loading