diff --git a/.github/workflows/nightly-install.yml b/.github/workflows/nightly-install.yml index 6c5b2f5de..9fedbc088 100644 --- a/.github/workflows/nightly-install.yml +++ b/.github/workflows/nightly-install.yml @@ -72,11 +72,22 @@ jobs: # (~/.kiro, ~/.claude, ~/.codex, ~/.config/opencode). With a fresh HOME # there is no Python 3.12 either, so this also exercises the script's # `uv python install` fallback every night. + # + # The script refuses, by design, when no supported AI tool is present + # ("No supported AI tools detected"), and the runner has none: no + # harness binary on PATH and nothing in the fresh HOME. So the fixture + # plants the four harness markers `detect_available_agent_tools()` + # reads without a binary -- ~/.kiro, ~/.claude, ~/.pi, ~/.grok -- and + # the install-agents step then exercises all four link sets for real. + # (Without this the job failed every night from 2026-09-15, before its + # verify steps ever ran; pinned by test_ci_workflow_contract.py.) env: HOME: ${{ runner.temp }}/home UV_TOOL_DIR: ${{ runner.temp }}/tools UV_TOOL_BIN_DIR: ${{ runner.temp }}/bin - run: mkdir -p "$HOME" && ./scripts/install.sh --non-interactive --no-smoke + run: | + mkdir -p "$HOME/.kiro" "$HOME/.claude" "$HOME/.pi" "$HOME/.grok" + ./scripts/install.sh --non-interactive --no-smoke - name: Verify installed CLI entry points env: @@ -84,3 +95,17 @@ jobs: run: | "$UV_TOOL_BIN_DIR/studyloop" --version "$UV_TOOL_BIN_DIR/session-export" --help + + - name: Verify installed agent definitions + # One artefact per planted harness, each a path `install agents` + # writes for that tool (installers.py _TOOL_LINKS / _configure_*). + env: + HOME: ${{ runner.temp }}/home + run: | + test -e "$HOME/.kiro/agents/study-mentor.json" + test -e "$HOME/.kiro/agents/study-plan-architect.json" + test -e "$HOME/.claude/agents/socratic-mentor.md" + test -e "$HOME/.claude/agents/study-plan-architect.md" + test -e "$HOME/.pi/agent/AGENTS.md" + test -e "$HOME/.grok/hooks/studyloop.json" + test -e "$HOME/.grok/rules/session-db.md" diff --git a/openspec/changes/plan-integration-followons/tasks.md b/openspec/changes/plan-integration-followons/tasks.md index a8f912e72..8f3b6d44b 100644 --- a/openspec/changes/plan-integration-followons/tasks.md +++ b/openspec/changes/plan-integration-followons/tasks.md @@ -228,7 +228,11 @@ writer, through the existing gate, closes that. Kept out of item 3 so item 3's f byte-equal afterwards; single `main` on both sides. (3) closeout comments — done: posted on #8–#15 from the re-verified draft (`receipts/issue-closeout-draft-2026-09-16.md`, status paragraph records the shas rewritten and the rows overtaken); #8, #9, #11, #12, #13, #14 closed as completed; #10 and #15 open (row 3b); #7 - reopened (auto-closed by PR #20's body) with the parent mapping. PR #20's body left as merged. **Open:** - (4) the two item-6 issues (after T6.4) and #21's closing comment or decision; (5) the local tag + reopened (auto-closed by PR #20's body) with the parent mapping. PR #20's body left as merged. (4) the two + item-6 issues — done 2026-09-19: **#25** (D-D derived plan bias) and **#26** (D-E nudge + retire/snooze), both + `ready-for-agent`, bodies drawn from the proposals with every cited test id, symbol and path checked against + `main` `a03fc9bd`; #21 given a status comment from the harness receipt and **left open** (its DoD names OpenCode + in core; OpenCode stays preview on the `opencode.db` exporter and the no-completed-reply gaps). **Open:** + (5) the local tag `archive/feat-clean-start-2026-09-15` — owner: push or discard; (6) the GitHub Support ticket text — owner; (7) revoke both tokens and delete `~/tmp/.env` — owner (D-J). diff --git a/packages/studyloop/tests/test_ci_workflow_contract.py b/packages/studyloop/tests/test_ci_workflow_contract.py index ebd150b8b..be7101fcd 100644 --- a/packages/studyloop/tests/test_ci_workflow_contract.py +++ b/packages/studyloop/tests/test_ci_workflow_contract.py @@ -236,3 +236,57 @@ def test_sast_and_pre_commit_bandit_skip_lists_agree() -> None: ) assert sast_skipped == precommit_skipped == ci_standards_skipped + + +NIGHTLY_WORKFLOW = WORKFLOW_DIR / "nightly-install.yml" + + +def _nightly_workflow() -> dict[str, Any]: + return yaml.safe_load(NIGHTLY_WORKFLOW.read_text(encoding="utf-8")) + + +def test_nightly_installer_job_plants_a_harness_before_running_install_sh() -> None: + """The nightly `installer` job (A4, added 2026-09-14) isolates HOME so + `studyloop install agents` writes into scratch -- and had never passed: + an empty HOME has no harness, `detect_available_agent_tools()` finds + nothing, and `install.sh` exits 1 at "Installing agent definitions" + exactly as the README says it should when no supported AI tool exists. + The job died five nights running before its verify step ever ran. + + The fixture must supply the precondition the script documents: at least + one harness marker directory that the detector reads without a binary + (`~/.kiro`, `~/.claude`, `~/.pi`, `~/.grok`), created in the isolated + HOME by the same step that runs the script. And the job must then check + what `install agents` wrote, or the isolation buys nothing. + """ + data = _nightly_workflow() + installer = data["jobs"]["installer"] + steps = installer["steps"] + run_step = next(step for step in steps if step.get("name") == "Run scripts/install.sh") + + assert run_step["env"]["HOME"] == "${{ runner.temp }}/home", ( + "HOME isolation is the point of the job; it must stay" + ) + run = run_step["run"] + assert "./scripts/install.sh --non-interactive --no-smoke" in run + + planted = [ + marker + for marker in ("$HOME/.kiro", "$HOME/.claude", "$HOME/.pi", "$HOME/.grok") + if marker in run + ] + assert planted, ( + "the installer job runs install.sh in an empty HOME; plant at least one " + "directory-detected harness marker first or `install agents` exits 1" + ) + plant_at = min(run.index(marker) for marker in planted) + assert plant_at < run.index("./scripts/install.sh"), "plant the marker BEFORE the script runs" + + verify = next( + (step for step in steps if step.get("name") == "Verify installed agent definitions"), + None, + ) + assert verify is not None, "the job must verify what `install agents` wrote into HOME" + assert verify["env"]["HOME"] == "${{ runner.temp }}/home" + for planted_marker in planted: + assert planted_marker in verify["run"], f"verify step does not look inside {planted_marker}"