Skip to content

Repository preparation

  • Lesson 2
  • 45 min
  • Module 1: Establish a safe working setup

What you will be able to do

Prepare the project before you ask an assistant for anything: a clean branch, an environment that you can make again, and checks that pass today. Then every change of the assistant shows as a diff, and every new failure is the change’s fault.

Before you startWhat to know and to have readyAbout 45 minutes

What you need to know

What you need on your computer

  • Python 3.12 or later, and Git. The course team tested the steps with Python 3.14.6 and Git 2.54 on macOS. The lesson also gives the Windows commands, but nobody tested them yet.
  • A terminal in your projects folder, ~/projects, and about 40 MB of free disk space.
  • No account, no key and no money.

When you pick up a rental car, an employee walks around it with you. Together you mark every scratch and dent on a drawing of the car, and you both sign it. You also write down the fuel level and the kilometres.

Why bother? Because when you bring the car back, the company checks it again. A scratch that is on the drawing was there before you. A scratch that is not on the drawing is yours. Without the drawing, nobody can tell, and you may pay for damage that you did not do.

Before an assistant changes your project, you need the same drawing.

The problem: the tests pass, and the report changed

Section titled “The problem: the tests pass, and the report changed”

Here is a real case from this course. The course team asked an assistant: “Add a way to filter the report by category.” It changed 4 files. Its own new tests passed, with the old ones: 29 passed. Nothing failed, so the change looked safe.

But the change also added a new key, "category": null, to every report, also when nobody asked for a category. Grace’s team reads that report into a spreadsheet. The extra key can break her sheet, and no test said a word. The project’s old tests check parts of the report, but no test said “the whole report must stay exactly as it is today”.

There is a second problem. Suppose a test had failed after the change. Was the test broken before the assistant started, or did the change break it? If you did not run the tests first, you cannot know. And if you had your own unsaved edits in the folder, you cannot even tell which lines are the assistant’s.

A clean starting point has three parts. Each part answers one question that you will ask after the assistant’s change.

  1. An environment that you can make again. The project runs in a new virtual environment, with the exact versions of requirements-lock.txt. Question it answers: “Does the change fail because of the code, or because of my computer?”
  2. A clean branch on a commit. You commit every file, and you work on a new branch. Question it answers: “Which lines did the assistant change?” The answer is git diff. To undo the change, git restore . puts every tracked file back as it is in the commit. A new file that the change added stays until you delete it.
  3. Baseline checks. The tests, ruff and a first run of the program pass, and you write down what they printed. Question it answers: “Is this failure new?”

A baseline is the known state of the project before a change, like the rental car’s drawing. One part of it can be a test. A regression test is a test that fails when behaviour that used to work changes. A software regression is a step back: something that worked stops working.

The program’s first run is easy to pin, because its output is a small JSON file. Today, python -m ticket_cleaner data/tickets.csv writes this report. Here, one line stands for the six rejected rows:

{
"status": "open",
"valid_records": 9,
"selected": 4,
"by_category": {"billing": 2, "login": 1, "shipping": 1},
"average_priority": 2.0,
"rejected": ["… 6 rows, each with its problem …"]
}

Nine of the 15 rows are valid. Four of them are open: two billing, one login and one shipping, with an average priority of 2.0. Imagine a test that compares the whole report with exactly these values. It fails if any key, count or problem changes, or if a new key appears. That is the drawing of the car.

You work in a new folder, ticket-cleaner-assist, which is a copy of the course’s start snapshot. It is the end of the Python course, with one change by a teammate. Now the report counts the new web form’s category names, such as Sign-in, under its own names. That change came with a bug, which Grace found. The course says so openly: you find the bug in Module 2 and fix it in Module 4. Your own ticket-cleaner from the Python course does not have it, so keep that folder as it is.

  1. Get the files of the course. If you cloned the labs repository for an earlier course, update it in ~/projects:

    Terminal window
    cd ~/projects
    git -C nextia-learning-labs pull

    If you do not have it yet, clone it in ~/projects with git clone https://github.com/Nextia-Intelligence/nextia-learning-labs.git. Without Git, download the ZIP file and unzip it in ~/projects. Its folder is then nextia-learning-labs-main: use that name in the next step.

  2. Copy the snapshot start to a new folder, and go into it:

    Terminal window
    cp -R nextia-learning-labs/C15/snapshots/start ticket-cleaner-assist
    cd ticket-cleaner-assist
  3. Make the virtual environment, activate it, and install the exact versions:

    Terminal window
    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install -r requirements-lock.txt

    The prompt now starts with (.venv), and pip prints a line that starts with Successfully installed.

  4. Make a Git repository, so that Git can show every change:

    Terminal window
    git init
    git status
    Output
    Initialized empty Git repository in /Users/amira/projects/ticket-cleaner-assist/.git/
    On branch main
    No commits yet
    Untracked files:
    (use "git add <file>..." to include in what will be committed)
    .env.example
    .gitignore
    LICENSE
    README.md
    data/
    pyproject.toml
    requirements-lock.txt
    requirements.txt
    tests/
    ticket_cleaner/
    nothing added to commit but untracked files present (use "git add" to track)

    .venv is not in the list, because the snapshot’s .gitignore excludes it. It also excludes reports/ and .env.

  5. Commit every file on main:

    Terminal window
    git add -A
    git commit -m "Start"
    Output (the start)
    [main (root-commit) 2498e15] Start
    25 files changed, 748 insertions(+)
    create mode 100644 .env.example
    create mode 100644 .gitignore

    Then come 23 more create mode lines. Your commit hash is different from 2498e15: it depends on your name and the time.

  6. Make the branch for Grace’s feature, and switch to it:

    Terminal window
    git switch -c feature/category-option
    git status
    Output
    Switched to a new branch 'feature/category-option'
    On branch feature/category-option
    nothing to commit, working tree clean

Now run the three checks and the first run, and keep what they print. These are the scratches on the drawing.

  1. Run the tests:

    Terminal window
    python -m pytest
    Output (the end)
    tests/test_cli.py .. [ 8%]
    tests/test_files.py .. [ 16%]
    tests/test_parsing.py .............. [ 75%]
    tests/test_remote.py .. [ 83%]
    tests/test_report.py .... [100%]
    ============================== 24 passed in 0.05s ==============================

    Your time can differ. If you see No module named pytest, read the common mistake at the end of this lesson.

  2. Run the linter and the format check:

    Terminal window
    ruff check .
    ruff format --check .
    Output
    All checks passed!
    15 files already formatted
  3. Run the program on the Python course’s sample file:

    Terminal window
    python -m ticket_cleaner data/tickets.csv
    Output
    WARNING ticket_cleaner: row 7 rejected: unknown status 'opne'
    WARNING ticket_cleaner: row 8 rejected: missing id
    WARNING ticket_cleaner: row 9 rejected: priority must be 1, 2 or 3, not 'high'
    WARNING ticket_cleaner: row 11 rejected: priority must be 1, 2 or 3, not '5'
    WARNING ticket_cleaner: row 13 rejected: duplicate id T-1003
    WARNING ticket_cleaner: row 14 rejected: missing category
    15 rows: 9 valid, 6 rejected.
    Report: reports/summary.json
  4. Check that the run did not change the repository:

    Terminal window
    git status --short

    The command prints nothing. The program wrote reports/summary.json, but .gitignore excludes reports/.

Write the four results in a note: 24 passed, All checks passed!, 15 files already formatted, and 15 rows: 9 valid, 6 rejected. After any change, you run the same four commands and compare.

Three boxes from left to right. On main, the commit Start: 24 passed, ruff all checks passed, 15 rows: 9 valid, 6 rejected. An arrow, git switch -c, leads to the branch feature/category-option and its commit Pin today's report of the sample file: 25 passed. An arrow leads to a dashed box: the assistant's change, not committed yet; git diff shows this change only. A red dashed arrow from the dashed box back to the commit says: git restore . goes back here.
A clean starting point. Every file is committed, the work happens on a branch, and the checks pass. An assistant's change is then the only difference that git diff shows, and git restore . removes it.
Text description of the diagram

The diagram has three boxes. The first is the commit Start on the branch main. Its checks passed: 24 tests, ruff, and the first run with 15 rows, 9 valid and 6 rejected. An arrow labelled git switch -c leads to the second box, on the branch feature/category-option. It is the commit “Pin today’s report of the sample file”, with 25 tests passing. You make that commit in the next section. An arrow leads to a dashed box: the assistant’s change, which is not committed yet. git diff shows this change and nothing else. A red dashed arrow goes from the dashed box back to the second commit, labelled “git restore . goes back here”.

The note is the drawing for you. A test is the drawing for everybody, and it checks itself every time someone runs pytest. The snapshot end-of-m01 has the test. Copy it into your project:

Terminal window
cp ../nextia-learning-labs/C15/snapshots/end-of-m01/tests/test_baseline.py tests/

Open tests/test_baseline.py and read it:

tests/test_baseline.py
"""Today's report for the sample file, kept as it is.
This test pins the report of data/tickets.csv before any change. If a change
makes it fail, that change altered the report: check that this was intended.
"""
import json
from ticket_cleaner.cli import main
def test_report_of_the_sample_file_is_unchanged(tmp_path):
output = tmp_path / "summary.json"
assert main(["data/tickets.csv", "--output", str(output)]) == 0
assert json.loads(output.read_text(encoding="utf-8")) == {
"status": "open",
"valid_records": 9,
"selected": 4,
"by_category": {"billing": 2, "login": 1, "shipping": 1},
"average_priority": 2.0,
"rejected": [
{"row": 7, "problems": ["unknown status 'opne'"]},
{"row": 8, "problems": ["missing id"]},
{"row": 9, "problems": ["priority must be 1, 2 or 3, not 'high'"]},
{"row": 11, "problems": ["priority must be 1, 2 or 3, not '5'"]},
{"row": 13, "problems": ["duplicate id T-1003"]},
{"row": 14, "problems": ["missing category"]},
],
}

The test runs the program as the command line does, with main(). It writes the report to tmp_path, a temporary folder that pytest makes for each test, so your own reports/ does not change. Then it compares the whole report with the values of today, with ==. One extra key, one changed count or one changed problem text makes it fail.

Run the checks, then commit the test:

Terminal window
python -m pytest
ruff check .
ruff format --check .
git add tests/test_baseline.py
git commit -m "Pin today's report of the sample file"
Output (the end of each command)
============================== 25 passed in 0.04s ==============================
All checks passed!
16 files already formatted
[feature/category-option 245d23e] Pin today's report of the sample file
1 file changed, 29 insertions(+)
create mode 100644 tests/test_baseline.py

Your project now matches the snapshot end-of-m01, except for ASSISTANT-SCOPE.md, which you write in the next lesson.

Now try the real change from the problem section. It is in the labs folder as A-vague-request.patch. The assistant wrote it in a recorded session, and the course did not change it. git apply puts it into your files, as an agent would:

Terminal window
git apply ../nextia-learning-labs/C15/candidates/A-vague-request.patch
git diff --stat
Output
tests/test_cli.py | 34 ++++++++++++++++++++++++++++++++++
tests/test_report.py | 23 +++++++++++++++++++++++
ticket_cleaner/cli.py | 6 +++++-
ticket_cleaner/report.py | 17 +++++++++++++++--
4 files changed, 77 insertions(+), 3 deletions(-)

git diff --stat shows the assistant’s change and nothing else, because you committed everything before it. Before you run the tests, predict: on start, this change gave 29 passed. What happens now?

Terminal window
python -m pytest
Output (the end)
E AssertionError: assert {'status': 'o...cted': 4, ...} == {'status': 'o...ing': 1}, ...}
E
E Omitting 6 identical items, use -vv to show
E Left contains 1 more item:
E {'category': None}
E Use -v to get more diff
tests/test_baseline.py:15: AssertionError
…
FAILED tests/test_baseline.py::test_report_of_the_sample_file_is_unchanged - ...
========================= 1 failed, 29 passed in 0.05s =========================

The baseline test failed, and pytest names the difference: the new report has one more item, {'category': None}. In JSON, Python’s None is null. The change of contract is now visible. Perhaps Grace wants it, but somebody must decide that, and update the test on purpose. Module 3 reviews this change line by line.

Undo the change. git restore . puts every tracked file back as it is in the last commit:

Terminal window
git restore .
git status
Output
On branch feature/category-option
nothing to commit, working tree clean
Two rows of four boxes. Top row, without a baseline test: start, 24 tests pass; apply the vague candidate, 4 files, plus 77 minus 3; python -m pytest, 29 passed, in green; every report now has category null and nothing failed, in red. Bottom row, with the baseline test: start plus test_baseline.py, 25 tests pass; apply the same candidate; python -m pytest, 1 failed, 29 passed, in red; test_baseline.py: Left contains 1 more item, category None; the change is caught, in green.
The same real change, applied twice. Without the baseline test, every test passes and the report has silently changed. With it, one test fails and names the new key.
Text description of the diagram

The diagram has two rows of four steps. The top row has no baseline test. It starts from start, where 24 tests pass. Next comes the vague candidate: 4 files, 77 lines added and 3 removed. python -m pytest prints 29 passed, in green. The last box, in red, says that every report now has “category”: null, and nothing failed. The bottom row has the baseline test: start plus test_baseline.py, where 25 tests pass. Next comes the same candidate. python -m pytest prints 1 failed, 29 passed, in red. The last box, in green, quotes the failure: Left contains 1 more item: {'category': None}. The change is caught.

Prepare like this before every assistant session that can change files: an agent, or a chat whose answer you paste in. The branch and the commit take a minute. The baseline test took one file of 29 lines.

There are costs. A test that pins the whole report fails for every change of the report, also for a wanted one. When Grace’s feature adds the key categories, it must appear only with --category, so this test keeps passing. A change that really should alter the default report must also update the test, in the same commit, so that a reviewer sees the decision. And a baseline pins today’s behaviour, bugs included. Do not pin an output that you know is wrong, as the next task shows.

A baseline does not replace review. It catches changes to what it pins, and nothing else. A change can pass all 25 tests and still be wrong in a way that no test checks.

Your turn: should you pin Grace’s export?

Section titled “Your turn: should you pin Grace’s export?”

Run the program on Grace’s export, the file of the new web form:

Terminal window
python -m ticket_cleaner data/web-form-export.csv

Open reports/summary.json and look at by_category. Tomás proposes a second baseline test for this report: “Then nothing can change it by mistake.” Before you write anything, decide: is this report a good baseline? What would the test do in Module 4, when you fix the bug? Do not commit a new test: the next lessons expect 25 tests.

Show the answer

The program prints 12 rows: 12 valid, 0 rejected. The report has:

"by_category": {
"Billing": 1,
"LOGIN": 1,
"Login": 1,
"account": 1,
"billing": 2,
"login": 2,
"shipping": 2
}

This is Grace’s bug: the report counts Billing, LOGIN and Login apart from billing and login. She expects account 1, billing 3, login 4 and shipping 2. A test that pins this report would protect the bug. In Module 4, the fix would make it fail, and the failure would look like a regression. So do not pin it. Write it in your note as “known wrong: Grace’s bug”, with the values she expects. A baseline pins only behaviour that you want to keep.

Common mistake

No module named pytest

Symptom: python -m pytest stops at once with a line such as this one. The path at the start is different on each computer:

/Library/Frameworks/Python.framework/Versions/3.14/bin/python3: No module named pytest

Cause: the virtual environment is not active, so the terminal uses the Python of your computer, which does not have the project’s packages. A new terminal window never has the environment active. Worse, a tool can exist outside the environment in another version. On the author’s computer, ruff --version printed ruff 0.16.0 outside the environment, and ruff 0.16.10 inside it, the version in requirements-lock.txt.

Fix: activate the environment again: source .venv/bin/activate on macOS and Linux, .venv\Scripts\Activate.ps1 on Windows. The prompt starts with (.venv). Then run the checks again. Run every check of this course with the environment active, so that your baseline uses the locked versions.

Common mistake

Your own edits mix with the assistant's

Symptom: you had added a note to README.md and not committed it. Then the candidate change went in. git diff --stat now shows 5 files and 79 lines added, not 4 files and 77 lines:

README.md | 2 ++
tests/test_cli.py | 34 ++++++++++++++++++++++++++++++++++
tests/test_report.py | 23 +++++++++++++++++++++++
ticket_cleaner/cli.py | 6 +++++-
ticket_cleaner/report.py | 17 +++++++++++++++--
5 files changed, 79 insertions(+), 3 deletions(-)

Cause: the working tree was not clean before the change. Git cannot tell your lines from the assistant’s: both are “changes since the last commit”. And git restore . removes both, so your note is lost too.

Fix: before every session, run git status. It must say nothing to commit, working tree clean. If it does not, commit your own work first, or keep it aside with git stash, which Recover work in the Git course explains.

Check your understanding

Name the three parts of a clean starting point, and the question that each part answers after an assistant's change.

Show the answer
  • An environment that you can make again (a new virtual environment from requirements-lock.txt): does the change fail because of the code, or because of my computer?
  • A clean branch on a commit (git status says working tree clean): which lines did the assistant change? git diff shows them, and git restore . removes them.
  • Baseline checks (the tests, ruff and a first run, written down or pinned in a test): is this failure new?

Check your understanding

Predict: on start, the vague candidate gives '29 passed'. With test_baseline.py, it gives '1 failed, 29 passed'. Why did the old tests not fail, and what did the new test see?

Show the answer

The old tests check parts of the report, such as one count or one key. The candidate’s own new tests check its own new behaviour. No test compared the whole report. The baseline test compares the whole report of data/tickets.csv with today’s values. The candidate adds "category": null to every report, so the test fails, and pytest names the extra item: {'category': None}.

Check your understanding

Practical check: show that your project is ready for an assistant. Which commands do you run, and what must they print?

Show the answer

In ~/projects/ticket-cleaner-assist, with the environment active:

  • git status prints On branch feature/category-option and nothing to commit, working tree clean.
  • git log --oneline shows two commits: Pin today's report of the sample file and Start.
  • python -m pytest prints 25 passed.
  • ruff check . prints All checks passed!, and ruff format --check . prints 16 files already formatted.
  • python -m ticket_cleaner data/tickets.csv prints 15 rows: 9 valid, 6 rejected.

Recap

  • Prepare before every session that can change files, like the drawing of a rental car’s scratches.
  • A clean starting point has three parts: an environment from the lock file, a clean branch on a commit, and baseline checks that pass.
  • tests/test_baseline.py pins the whole report of the sample file. A real assistant change passed every old test (29 passed), but it added "category": null to every report. With the baseline, 1 failed, 29 passed named the extra key.
  • git diff --stat shows only the assistant’s change when the tree was clean before, and git restore . removes it. Uncommitted work of your own would mix in, and git restore . would delete it too.
  • A baseline pins today’s behaviour, bugs included: do not pin Grace’s split categories.

Your project now has 25 passed on the branch feature/category-option. Next, you write down what an assistant may read, change and run in it.

Next: Context and boundaries