testsAndMisc/linux_configuration/utils/lib/tests/test_study_gen.sh
Krzysztof kuhy Rudnicki 4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:53:30 +02:00

245 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# Tests for the three generation phases: lib/study_gen_docs.sh,
# lib/study_gen_anki.sh and lib/study_gen_llm.sh.
#
# Each phase reads the grep_*.txt files the analysis step wrote and produces
# one output file. The cases below pin what actually matters about each output
# rather than its exact bytes: that every extracted term appears, that the Anki
# file keeps the tab-separated shape its importer requires, and that the LLM
# prompt keeps the instructions that stop an assistant answering from memory
# instead of from the docs it was told to read.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=study_harness.sh
. "${SCRIPT_DIR}/study_harness.sh"
# shellcheck source=../study_doc_urls.sh
. "${SCRIPT_DIR}/../study_doc_urls.sh"
# shellcheck source=../study_doc_urls_more.sh
. "${SCRIPT_DIR}/../study_doc_urls_more.sh"
# shellcheck source=../study_doc_lookup.sh
. "${SCRIPT_DIR}/../study_doc_lookup.sh"
# shellcheck source=../study_gen_docs.sh
. "${SCRIPT_DIR}/../study_gen_docs.sh"
# shellcheck source=../study_gen_anki.sh
. "${SCRIPT_DIR}/../study_gen_anki.sh"
# shellcheck source=../study_gen_llm.sh
. "${SCRIPT_DIR}/../study_gen_llm.sh"
echo "== generate_doc_links: writes a section per term kind =="
reset_state
stage_results
generate_doc_links >/dev/null
docs="$(cat "${DOCS_FILE}")"
_t_has "$docs" "def" "a keyword appears in the doc index"
_t_has "$docs" "print" "a function call appears"
_t_has "$docs" "os" "an import appears"
_t_has "$docs" "docs.python.org" "terms carry a resolved documentation link"
echo "== generate_doc_links: honours TOP_N =="
reset_state
printf '%s\n' "9 alpha" "8 bravo" "7 charlie" "6 delta" >"${RESULTS_DIR}/grep_keywords.txt"
: >"${RESULTS_DIR}/grep_function_calls.txt"
: >"${RESULTS_DIR}/grep_imports.txt"
TOP_N=2
generate_doc_links >/dev/null
docs="$(cat "${DOCS_FILE}")"
_t_has "$docs" "alpha" "the top term is included"
if [[ "$docs" == *"charlie"* ]]; then
_t_fail "a term past TOP_N is excluded"
else
_t_pass "a term past TOP_N is excluded"
fi
echo "== generate_doc_links: missing input files are not fatal =="
reset_state
# A repo with no imports at all is normal, and the analysis simply does not
# write that file. The phase must still produce its other sections.
printf '%s\n' "9 def" >"${RESULTS_DIR}/grep_keywords.txt"
generate_doc_links >/dev/null
_t_has "$(cat "${DOCS_FILE}")" "def" "the surviving section is still written"
echo "== generate_doc_links: uses the offline mirror when installed =="
reset_state
stage_results
enable_offline_docs "${OFFLINE_DOCS_DIR}/python/os.md"
generate_doc_links >/dev/null
_t_has "$(cat "${DOCS_FILE}")" "${OFFLINE_DOCS_DIR}" "cards cite a local path an assistant can read"
echo "== generate_doc_links: per-language mode =="
reset_state
stage_results
# The presence of per_language/ switches the phase onto an entirely separate
# path that resolves each language's terms against that language's docs.
stage_per_language python javascript rust
generate_doc_links >/dev/null
docs="$(cat "${DOCS_FILE}")"
_t_has "$docs" "Language Keywords" "the per-language section header is written"
_t_has "$docs" "Python" "a language gets a display name"
_t_has "$docs" "JavaScript" "the JavaScript display name is mapped"
_t_has "$docs" "Rust" "a third language is included"
_t_has "$docs" "alpha_python" "a per-language keyword is listed"
_t_has "$docs" "fn_javascript" "a per-language function is listed"
_t_has "$docs" "mod_rust" "a per-language import is listed"
echo "== generate_doc_links: per-language name mapping =="
reset_state
stage_results
stage_per_language c_cpp typescript shell
generate_doc_links >/dev/null
docs="$(cat "${DOCS_FILE}")"
_t_has "$docs" "C/C++" "c_cpp maps to the C/C++ display name"
_t_has "$docs" "TypeScript" "typescript maps to its display name"
_t_has "$docs" "Shell/Bash" "shell maps to the Shell/Bash display name"
echo "== generate_doc_links: per-language covers the remaining names =="
reset_state
stage_results
stage_per_language go ruby java klingon
generate_doc_links >/dev/null
docs="$(cat "${DOCS_FILE}")"
_t_has "$docs" "Go" "go maps to its display name"
_t_has "$docs" "Ruby" "ruby maps to its display name"
_t_has "$docs" "Java" "java maps to its display name"
_t_has "$docs" "klingon" "an unmapped language falls back to its raw name"
echo "== generate_doc_links: per-language skips empty and comment lines =="
reset_state
stage_results
stage_per_language python
# An empty file and a comment-only line are both normal analysis output; each
# must be skipped rather than producing a card with no term.
: >"${RESULTS_DIR}/per_language/functions_python.txt"
printf '%s\n' "# a comment" "5 realterm" >"${RESULTS_DIR}/per_language/imports_python.txt"
generate_doc_links >/dev/null
docs="$(cat "${DOCS_FILE}")"
_t_has "$docs" "realterm" "the real term survives"
if [[ "$docs" == *"a comment"* ]]; then
_t_fail "a comment line is skipped"
else
_t_pass "a comment line is skipped"
fi
echo "== generate_anki_cards: tab-separated with a header =="
reset_state
stage_results
generate_anki_cards >/dev/null
anki="$(cat "${ANKI_FILE}")"
_t_has "$anki" "Anki Import File" "the importer header is present"
_t_has "$anki" "Fields separated by: Tab" "the header states the separator"
_t_has "$anki" "def" "a keyword card is written"
_t_has "$anki" "print" "a function card is written"
# Every non-comment line must carry at least one tab, or the import silently
# collapses the card into a single field.
bad=0
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
[[ "$line" == *$'\t'* ]] || bad=$((bad + 1))
done <"${ANKI_FILE}"
_t_eq "0" "$bad" "every card line is tab-separated"
echo "== generate_anki_cards: honours TOP_N =="
reset_state
printf '%s\n' "9 alpha" "8 bravo" "7 charlie" >"${RESULTS_DIR}/grep_keywords.txt"
: >"${RESULTS_DIR}/grep_function_calls.txt"
TOP_N=1
generate_anki_cards >/dev/null
anki="$(cat "${ANKI_FILE}")"
_t_has "$anki" "alpha" "the top term becomes a card"
if [[ "$anki" == *"bravo"* ]]; then
_t_fail "a term past TOP_N is excluded"
else
_t_pass "a term past TOP_N is excluded"
fi
echo "== generate_anki_cards: no input files still writes a valid file =="
reset_state
generate_anki_cards >/dev/null
_t_has "$(cat "${ANKI_FILE}")" "Anki Import File" "an empty analysis still produces an importable file"
echo "== generate_anki_cards: one card template per keyword category =="
reset_state
# Each category gets its own answer text, so a card for `if` explains control
# flow rather than repeating a generic definition. Covering every arm is what
# proves no category silently falls through to the default template.
printf '%s\n' "9 if" "8 for" "7 try" "6 class" "5 async" "4 frobnicate" \
>"${RESULTS_DIR}/grep_keywords.txt"
: >"${RESULTS_DIR}/grep_function_calls.txt"
TOP_N=6
generate_anki_cards >/dev/null
anki="$(cat "${ANKI_FILE}")"
_t_has "$anki" "Conditional control flow" "a conditional keyword gets the control-flow answer"
_t_has "$anki" "Loop construct" "a loop keyword gets the loop answer"
_t_has "$anki" "Exception handling" "an exception keyword gets the exception answer"
_t_has "$anki" "Type definition" "a type keyword gets the type answer"
_t_has "$anki" "Asynchronous programming" "an async keyword gets the async answer"
_t_has "$anki" "frobnicate" "an uncategorised keyword still gets a card"
echo "== generate_llm_prompt: project-internal items are skipped =="
reset_state
# A relative or aliased import points at the repo being studied, not at any
# documentation, so a flashcard for it could only be answered from guesswork.
# These must be marked skippable rather than sent to the assistant as a topic.
printf '%s\n' "9 ./local_module" "8 @/aliased" "7 src/thing" "6 app.config" "5 os" \
>"${RESULTS_DIR}/grep_imports.txt"
printf '%s\n' "9 def" >"${RESULTS_DIR}/grep_keywords.txt"
printf '%s\n' "8 print" >"${RESULTS_DIR}/grep_function_calls.txt"
TOP_N=5
generate_llm_prompt >/dev/null
_t_has "$(cat "${LLM_PROMPT_FILE}")" "INTERNAL - SKIP" "an internal import is marked skippable"
echo "== generate_llm_prompt: single-letter functions are dropped =="
reset_state
# Minified JS turns every function into one or two characters. A flashcard for
# `a` teaches nothing and cannot be looked up, so those are dropped rather than
# sent to the assistant as topics.
printf '%s\n' "9 def" >"${RESULTS_DIR}/grep_keywords.txt"
printf '%s\n' "9 a" "8 b" "7 realFunction" >"${RESULTS_DIR}/grep_function_calls.txt"
printf '%s\n' "6 os" >"${RESULTS_DIR}/grep_imports.txt"
TOP_N=5
generate_llm_prompt >/dev/null
prompt="$(cat "${LLM_PROMPT_FILE}")"
_t_has "$prompt" "realFunction" "a real function name survives"
if [[ "$prompt" =~ [0-9]+\ a$'\n' ]]; then
_t_fail "a single-letter function is dropped"
else
_t_pass "a single-letter function is dropped"
fi
echo "== generate_llm_prompt: keeps the read-the-docs instructions =="
reset_state
stage_results
generate_llm_prompt >/dev/null
prompt="$(cat "${LLM_PROMPT_FILE}")"
# These four instructions are the entire reason the prompt exists: without
# them an assistant writes flashcards from memory, which is exactly the
# failure the offline-docs pipeline was built to prevent.
_t_has "$prompt" "READ DOCS VIA TERMINAL" "the read-the-file instruction survives"
_t_has "$prompt" "DO NOT USE YOUR OWN KNOWLEDGE" "the no-memory instruction survives"
_t_has "$prompt" "IF YOU CANNOT READ A FILE" "the unreadable-file instruction survives"
_t_has "$prompt" "NEVER FALL BACK TO GENERAL KNOWLEDGE" "the no-fallback instruction survives"
_t_has "$prompt" "def" "the extracted keywords are listed"
echo "== generate_llm_prompt: import lines are handled as imports =="
reset_state
stage_results
printf '%s\n' "12 import os" "7 from sys import argv" >"${RESULTS_DIR}/grep_imports.txt"
generate_llm_prompt >/dev/null
_t_has "$(cat "${LLM_PROMPT_FILE}")" "os" "a whole import statement still resolves to its module"
echo "== generate_llm_prompt: offline paths are cited when available =="
reset_state
stage_results
enable_offline_docs "${OFFLINE_DOCS_DIR}/python/os.md"
generate_llm_prompt >/dev/null
_t_has "$(cat "${LLM_PROMPT_FILE}")" "${OFFLINE_DOCS_DIR}" "the prompt cites readable local paths"
echo "== generate_llm_prompt: no inputs still writes the instructions =="
reset_state
generate_llm_prompt >/dev/null
_t_has "$(cat "${LLM_PROMPT_FILE}")" "DO NOT USE YOUR OWN KNOWLEDGE" \
"an empty analysis still produces a usable prompt"
_t_summary