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
39 changes: 36 additions & 3 deletions tests/test_lane_helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ done
[ -f "$SRC_DIR/repos.tsv" ] || { echo "missing: $SRC_DIR/repos.tsv" >&2; exit 1; }

SANDBOX="$(mktemp -d)"
# Set before the traps below, so `cleanup` never reads an inherited value.
A11T_ROOT=""
REAL_HOME="$HOME"

# THE GUARDS AT THE FOOT OF THIS FILE, re-pointed (Amendment 9, act 3
Expand Down Expand Up @@ -96,6 +98,9 @@ cleanup() {
[ -n "${RACE_PARENT:-}" ] && kill "$RACE_PARENT" 2>/dev/null
[ -n "${RACE_CHILD:-}" ] && kill "$RACE_CHILD" 2>/dev/null
[ -n "${GD_DUP:-}" ] && kill "$GD_DUP" 2>/dev/null
# #180's short checkout root for the clause (c) TAIL case, which is outside
# `$SANDBOX` wherever the run root is long.
[ -n "${A11T_ROOT:-}" ] && [ -d "$A11T_ROOT" ] && rm -rf -- "$A11T_ROOT"
[ -n "${SANDBOX:-}" ] && [ -d "$SANDBOX" ] && rm -rf -- "$SANDBOX"
return 0
}
Expand Down Expand Up @@ -5348,12 +5353,40 @@ has "…keeping the home and estate the line already carried" "$A11B_LOG" "home
FAKE_TMUX_WINDOW="a11start:@42" FAKE_TMUX_WINDOW_INDEX=3 FAKE_TMUX_WINDOW_NAME=repoA11b-1 \
CLAUDE_PROFILE_NAME=team-05a run "$START" repoA11b 1 --no-launch
is "lane-start exits 0 on a second start of the same lane" "$rc" 0
has "the row's stamp states clause (c)'s two facts for a person, in its TAIL" \
"$(grep '^| `repoA11b-1`' "$LANES")" "dir $HOME/projects/repoA11b; window a11start:3 @42"
has "…and that tail still says what actually happened, so --no-launch never claims a launch" \
has "…and its stamp says what actually happened, so --no-launch never claims a launch" \
"$(grep '^| `repoA11b-1`' "$LANES")" ", no launch"
has "…while its HEAD is Amendment 8(d)'s and is untouched" \
"$(grep '^| `repoA11b-1`' "$LANES")" "(lane repoA11b-1) — lane-start on Eagle: window renamed, no launch; dir "
# THE TAIL IS READ ON A CHECKOUT AT A SHORT PATH (opensoft/openRepoTools#180).
# The stamp is ONE line under ratified decision O1's cap (Amendment 13(a)), and
# `cut_to_line` takes `dir` and `window` first by design — both are this same
# act's log sub-fields, asserted in full above — so the tail fits only when the
# CHECKOUT PATH does. Read on `$HOME/projects/repoA11b`, this case's answer was
# the run root's length: green under plain pytest's `/tmp`, red under
# `tests/run.sh`'s per-run root (`… no launch; dir ...`, the path cut whole), at
# the edge on macOS. The checkout goes in this sandbox's own `$TMPDIR` where
# that is at most 40 characters, as under plain pytest on Linux, and in `/tmp`
# where it is not, so `$A11T_DIR` is never over 65 characters and the line never
# over about 210, inside the 230 the cut keeps wherever the root is. `cleanup`
# removes it; only a SIGKILLed run under a long root leaves one in `/tmp`.
# `tests/test_lane_start_row_stamp.py` drives the same stamp at three checkout
# lengths, the two that are cut included.
a11t_base="$TMPDIR"
[ "${#a11t_base}" -le 40 ] || a11t_base=/tmp
A11T_ROOT="$(mktemp -d "$a11t_base/ort-a11t.XXXXXX" 2>/dev/null || :)"
A11T_DIR="${A11T_ROOT:-$SANDBOX/a11t-no-short-root}/repoA11t"
is "the TAIL case has a checkout of at most 65 characters, wherever the run root is" \
"$( [ -n "$A11T_ROOT" ] && [ -d "$A11T_ROOT" ] && [ "${#A11T_DIR}" -le 65 ] && echo yes || echo no )" "yes"
git init -q -b main "$A11T_DIR"
git -C "$A11T_DIR" remote add origin "https://github.com/opensoft/repoA11t.git"
FAKE_TMUX_WINDOW="a11tail:@43" FAKE_TMUX_WINDOW_INDEX=5 FAKE_TMUX_WINDOW_NAME=claude \
run "$START" --dir "$A11T_DIR" repoA11t 1 --no-launch
is "lane-start exits 0 for a lane whose checkout is at that short path" "$rc" 0
FAKE_TMUX_WINDOW="a11tail:@43" FAKE_TMUX_WINDOW_INDEX=5 FAKE_TMUX_WINDOW_NAME=repoA11t-1 \
run "$START" repoA11t 1 --no-launch
is "…and on its second start, which reads the checkout back from the lane's log" "$rc" 0
has "the row's stamp states clause (c)'s two facts for a person, in its TAIL" \
"$(grep '^| `repoA11t-1`' "$LANES")" "dir $A11T_DIR; window a11tail:5 @43"
# NO PROFILE IS NO SUB-FIELD, never a guessed one (Amendment 7(i)).
mkdir -p "$HOME/projects/repoA11c"
git init -q -b main "$HOME/projects/repoA11c"
Expand Down
273 changes: 273 additions & 0 deletions tests/test_lane_start_row_stamp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
# SPDX-License-Identifier: Apache-2.0
"""`lane-start`'s row stamp under a long checkout path (opensoft/openRepoTools#180).

THE STAMP'S TAIL IS THE PART A CUT TAKES, AND THAT IS THE DESIGN. Amendment
11(c) puts the lane's `dir` and `window` in the tail of the stamp a resume
writes; Amendment 13(a) makes the cell ONE line under ratified decision O1's
240 characters; and `lane-start` orders that line so `cut_to_line` takes `dir`
and `window` first, because both are this same act's sub-fields in the lane's
own log and columns of Amendment 11's listing — a cut loses what a reader can
look up, never the head `row_stamp_is_unknown` reads.

So the tail's length is the CHECKOUT PATH's length, and #180 was a shell-suite
case that put its checkout under `$HOME/projects` inside the run root: green
under plain pytest's short `/tmp`, red under `tests/run.sh`'s per-run root
(`TMPDIR` about 71 characters, so a `$HOME` about 91). These cases drive a real
`lane-start` twice, `--no-launch`, in that shape — a `$HOME` at least as long
as `run.sh`'s — with the checkout at three EXACT lengths, so the answer does not
depend on where pytest itself was run:

* a short checkout keeps the whole tail, however long `$HOME` is — which is
what `tests/test_lane_helpers.sh`'s clause (c) TAIL case now uses, on a
checkout of at most 65 characters;
* a checkout whose path still fits loses only the `window` ref;
* a longer one loses the path WHOLE, never a truncated path that reads as a
real one. That is what #180's case read under `run.sh`'s root:
`… window renamed, no launch; dir ...`, its cut landing on the `;` that
follows the path, so the path went with it;

and in every one the head is intact, the line is inside the cap, and the
lane's log carries both facts in full.
"""

from __future__ import annotations

import os
import shutil
import subprocess
import tempfile
from pathlib import Path

import pytest

from conftest import REPO, WINDOWS_SKIP, rmtree

pytestmark = [pytest.mark.skipif(shutil.which("bash") is None,
reason="lane-start is a bash script"),
pytest.mark.skipif(shutil.which("git") is None,
reason="the register is a git repository"),
WINDOWS_SKIP]

LANE = "repoZ-1"
WINDOW = "testsess:0 @1"

#: `tests/run.sh`'s `$HOME` for the shell suite, as #180 measured it: a run
#: root `TMPDIR` of about 71 characters, the suite's `mktemp -d` beneath it
#: (`/tmp.XXXXXXXXXX`, 15) and `/home` (5).
RUN_SH_HOME = 91

SCRUBBED_PREFIXES = ("LANES_", "LANE_START_", "CLAUDE_", "OPENREPOTOOLS_",
"FAKE_", "GIT_", "TMUX")
SCRUBBED = {"AGENT_PROTOCOL_ROOT", "PROJECTS_ROOT", "XDG_CONFIG_HOME",
"XDG_CACHE_HOME"}

#: The lane suite's fake tmux, cut down to one window, `testsess:0 @1`, whose
#: name is `$FAKE_TMUX_WINDOW_NAME` — `claude` on a first start and the lane
#: on the second, as a window lane-start renamed would answer.
FAKE_TMUX = r"""#!/usr/bin/env bash
case "${1-}" in
display-message)
shift
t=""; f=""
while [ $# -gt 0 ]; do
case "$1" in
-p) shift ;;
-t) t="${2-}"; shift 2 ;;
*) f="$1"; shift ;;
esac
done
if [ -n "$t" ]; then
case "$t" in
testsess:0|testsess:@1|@1) : ;;
*) exit 1 ;;
esac
case "$f" in
'#{window_id}') echo '@1' ;;
'#{window_name}') echo "${FAKE_TMUX_WINDOW_NAME:-claude}" ;;
'#{session_name}') echo 'testsess' ;;
'#{pane_current_command}') echo 'claude' ;;
*) echo ;;
esac
exit 0
fi
case "$f" in
'#{session_name}:#{window_id}') echo 'testsess:@1' ;;
'#{session_name}:#{window_index}') echo 'testsess:0' ;;
'#S:#I') echo 'testsess:0' ;;
'#{window_id}') echo '@1' ;;
'#W'|'#{window_name}') echo "${FAKE_TMUX_WINDOW_NAME:-claude}" ;;
*) echo ;;
esac ;;
rename-window)
printf 'rename-window %s\n' "${2-}" >> "$FAKE_TMUX_LOG" ;;
*) : ;;
esac
"""

#: The workspace repository's first commit: a register with its header and no
#: rows, and the handoffs directory a row's handoff path points into.
WORKSPACE_SEED = {
"lanes/LANES.md": "\n".join((
"# LANES.md -- the sandbox register", "",
"| lane | session id | workstation / env / user | started (UTC) | "
"objects owned | handoff path | state |",
"|---|---|---|---|---|---|---|", "")),
"handoffs/README.md": "# handoffs\n",
}

FAKE_GH = """#!/usr/bin/env bash
printf 'FAKE gh REFUSED: %s -- this suite reaches no GitHub surface\\n' "$*" >&2
exit 90
"""


def _write(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
path.chmod(0o755)


class Sandbox:
"""One workstation, one register, and a `$HOME` at least `run.sh`'s length."""

def __init__(self, tmp_path: Path) -> None:
pad = max(0, RUN_SH_HOME - len(str(tmp_path / "r" / "home")))
self.root = tmp_path / ("r" * (pad + 1))
self.home = self.root / "home"
assert len(str(self.home)) >= RUN_SH_HOME
self.bin = self.root / "bin"
fakebin = self.root / "fakebin"
origin = self.root / "origin.git"
self.wip = self.home / "projects" / "wip"
for d in (self.home / "projects", self.root / "tmp", fakebin, self.bin,
self.home / ".claude" / "sessions", self.home / ".agents"):
d.mkdir(parents=True, exist_ok=True)

env = {k: v for k, v in os.environ.items()
if k not in SCRUBBED and not k.startswith(SCRUBBED_PREFIXES)}
env.update({
"HOME": str(self.home),
"TMPDIR": str(self.root / "tmp"),
"XDG_CONFIG_HOME": str(self.home / ".config"),
"XDG_CACHE_HOME": str(self.home / ".cache"),
"PATH": f"{fakebin}{os.pathsep}{self.bin}{os.pathsep}{env.get('PATH', '')}",
"TMUX": f"{self.root}/fake-tmux-socket,0,0",
"AGENT_PROTOCOL_ROOT": str(self.home / ".agents"),
"OPENREPOTOOLS_BIN_DIR": str(self.bin),
"CLAUDE_CONFIG_DIR": str(self.home / ".claude"),
"LANES_WORKSTATION": "Eagle",
"LANES_NO_GITHUB": "1",
"LANES_INDEX": "off",
"FAKE_TMUX_LOG": str(self.root / "tmux.log"),
"GIT_AUTHOR_NAME": "lane-start row stamp tests",
"GIT_AUTHOR_EMAIL": "test@example.invalid",
"GIT_COMMITTER_NAME": "lane-start row stamp tests",
"GIT_COMMITTER_EMAIL": "test@example.invalid",
})
self.env = env

for name in ("lane-start", "lanes-edit.sh", "repos.tsv"):
shutil.copy2(REPO / name, self.bin / name)
(self.bin / name).chmod(0o755)
_write(fakebin / "tmux", FAKE_TMUX)
_write(fakebin / "gh", FAKE_GH)
_write(fakebin / "claude", "#!/usr/bin/env bash\nexit 0\n")

self.git("init", "-q", "--bare", "-b", "main", str(origin))
self.git("clone", "-q", str(origin), str(self.wip))
for rel, text in WORKSPACE_SEED.items():
(self.wip / rel).parent.mkdir(parents=True, exist_ok=True)
(self.wip / rel).write_text(text, encoding="utf-8")
for step in (("add", "--", *WORKSPACE_SEED), ("commit", "-q", "-m", "seed"),
("push", "-q", "origin", "main")):
self.git("-C", str(self.wip), *step)
(self.home / ".agents" / "workspace.yaml").write_text(
f"repository: {origin}\npath: {self.wip}\n", encoding="utf-8")

def git(self, *args: str) -> str:
return subprocess.run(["git", *args], env=self.env, check=True,
capture_output=True, text=True).stdout

def checkout(self, path: Path) -> None:
self.git("init", "-q", "-b", "main", str(path))
self.git("-C", str(path), "remote", "add", "origin",
"https://github.com/opensoft/repoZ.git")

def start(self, *args: str, window_name: str) -> subprocess.CompletedProcess:
return subprocess.run([str(self.bin / "lane-start"), "repoZ", "1",
"--no-launch", *args],
env=dict(self.env, FAKE_TMUX_WINDOW_NAME=window_name),
capture_output=True, text=True, timeout=120,
check=False)

def stamp_line(self) -> str:
"""The `<one line>` of the row's `<STATE> · <UTC> · <one line>` cell."""
rows = [r for r in (self.wip / "lanes" / "LANES.md")
.read_text(encoding="utf-8").splitlines()
if r.startswith(f"| `{LANE}`")]
assert len(rows) == 1, rows
cell = rows[0].rstrip().rstrip("|").rstrip().rsplit(" | ", 1)[1]
state, _utc, line = cell.split(" · ", 2)
assert state == "LIVE", cell
return line

def last_log_line(self) -> str:
return (self.wip / "lanes" / "log" / f"{LANE}.md").read_text(
encoding="utf-8").splitlines()[-1]


@pytest.fixture
def short_root():
"""A short, FIXED-length directory outside the run root: `/tmp/` and
`mkdtemp`'s name, whatever `TMPDIR` or pytest's basetemp say."""
root = Path(tempfile.mkdtemp(prefix="ort-tail.", dir="/tmp"))
try:
yield root
finally:
rmtree(root)


def _checkout_of_length(root: Path, length: int | None) -> Path:
"""`<root>/repoZ`, padded between the two to exactly `length` characters."""
if length is None:
return root / "repoZ"
pad = length - len(str(root)) - len("/repoZ") - 1
assert pad > 0, (root, length)
path = root / ("p" * pad) / "repoZ"
assert len(str(path)) == length
return path


@pytest.mark.parametrize("length, keeps", [
pytest.param(None, "dir and window", id="short-fixed-checkout"),
pytest.param(100, "dir", id="window-cut-dir-kept"),
pytest.param(160, "nothing", id="path-dropped-whole-as-in-180"),
])
def test_the_stamp_tail_is_cut_by_the_checkout_path_and_never_the_head(
tmp_path, short_root, length, keeps):
box = Sandbox(tmp_path)
lane_dir = _checkout_of_length(short_root, length)
box.checkout(lane_dir)

first = box.start("--dir", str(lane_dir), window_name="claude")
assert first.returncode == 0, first.stderr
# THE SECOND START STAMPS (Amendment 6(c)), and it finds the checkout in
# the lane's own log, exactly as the shell suite's case does.
second = box.start(window_name=LANE)
assert second.returncode == 0, second.stderr
line = box.stamp_line()

assert len(line) <= 240, line
assert (f"(lane {LANE}) — lane-start on Eagle: window renamed, "
f"no launch; dir ") in line, line
if keeps == "dir and window":
assert line.endswith(f"; dir {lane_dir}; window {WINDOW}"), line
elif keeps == "dir":
assert f"; dir {lane_dir};" in line and line.endswith(" ..."), line

Check warning on line 266 in tests/test_lane_start_row_stamp.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Split this composite assertion into separate assertions.

See more on https://sonarcloud.io/project/issues?id=opensoft_openRepoTools&issues=AaE2lcD6xgGyo09e-0Yb&open=AaE2lcD6xgGyo09e-0Yb&pullRequest=215
assert WINDOW not in line, line
else:
assert line.endswith("no launch; dir ..."), line
assert str(short_root) not in line, line

# NOTHING IS LOST: what the cut took is this act's own log line.
assert f"; dir {lane_dir}; window {WINDOW}; " in box.last_log_line()
Loading