Compare commits
10 Commits
61d00a6992
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b0548f0e56 | |||
| 4dda6de387 | |||
| 8c7a67c471 | |||
| 319e8b84f4 | |||
| 8d1379ec66 | |||
| 45257057df | |||
| aa5335aca0 | |||
| 1d8f16303e | |||
| 1e1c124c9f | |||
| 1a98c14654 |
@@ -1,8 +1,9 @@
|
||||
# STEM Launcher
|
||||
|
||||
`stemctl` jest hostowym wejściem do kart pracy, Git i trzech źródłowo
|
||||
budowanych kontenerów STEM. Historyczna nazwa zdalnego repo może nadal brzmieć
|
||||
`rv-launcher`; `rvctl` pozostaje ostrzegającym wrapperem zgodności.
|
||||
budowanych kontenerów STEM. Zdalne repo nosi kanoniczną nazwę
|
||||
`edu-tools/stem-launcher`; Gitea przekierowuje historyczny URL `rv-launcher`,
|
||||
a `rvctl` pozostaje cichym wrapperem zgodności.
|
||||
|
||||
Launcher nie pobiera i nie publikuje gotowych obrazów środowiska. Klonuje repo
|
||||
narzędzi zawierające `Dockerfile` i `docker-compose.yml`, a brakujący profil
|
||||
@@ -22,6 +23,23 @@ Neovim, Termdebug, nvim-dap oraz MCP tmuxa i Neovima. Kod karty, wyniki i
|
||||
artefakty są bind-mountem na hoście. Kontenery nie dostają kluczy SSH, tokenów,
|
||||
socketu Podmana/Dockera ani całego `/dev`.
|
||||
|
||||
## Pierwsza instalacja
|
||||
|
||||
Pierwszy klon launchera również należy do workspace. Nie uruchamiamy
|
||||
`stemctl` z przypadkowego katalogu domowego ani z kontenera:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/dev/workspace/stem/tools
|
||||
git clone http://77.90.8.171:3001/edu-tools/stem-launcher.git \
|
||||
~/dev/workspace/stem/tools/stem-launcher
|
||||
cd ~/dev/workspace/stem/tools/stem-launcher
|
||||
./stemctl workspace sync
|
||||
```
|
||||
|
||||
Od tego momentu wszystkie komendy `stemctl`, karty i repozytoria odpowiedzi
|
||||
pozostają pod `~/dev/workspace/stem`. Kontener dostaje wybraną kartę jako
|
||||
`/workspace`, ale nie jest miejscem przechowywania źródeł.
|
||||
|
||||
## Szybki start
|
||||
|
||||
```bash
|
||||
@@ -41,6 +59,23 @@ socketu Podmana/Dockera ani całego `/dev`.
|
||||
Pierwsze wywołanie danego profilu może potrwać, ponieważ buduje go ze
|
||||
źródłowego Dockerfile. Kolejne korzystają z lokalnych warstw cache.
|
||||
|
||||
Pełną sesję lekcji można wybrać i kontrolować numerami z CLI:
|
||||
|
||||
```bash
|
||||
./stemctl session choices --series inf --card 7 --task 4
|
||||
./stemctl session reset hazard3-sim --series inf --card 7 --task 4 --step 12
|
||||
./stemctl session refresh hazard3-sim --series inf --card 7 --task 4
|
||||
./stemctl session attach hazard3-sim --series inf --card 7 --task 4
|
||||
./stemctl session detach hazard3-sim --series inf --card 7 --task 4
|
||||
```
|
||||
|
||||
`current:0` jest domyślnym pane: pane `0` okna tmuxa, w którym działa
|
||||
wywołujący Codex. `session stage`/`session checkpoint` ustawia dowolny etap UML
|
||||
online w Termdebug albo tylko na stronie z `--offline`. Semantykę poleceń,
|
||||
w tym podpinanie kontenera przez `attach/detach`, opisuje
|
||||
[referencja stemctl](doc/stemctl.md), a wybór checkpointów
|
||||
[dokumentacja sesji](doc/session.md).
|
||||
|
||||
Wymuszenie samego przygotowania środowiska:
|
||||
|
||||
```bash
|
||||
@@ -68,6 +103,33 @@ Domyślny układ:
|
||||
└── tokens/tokens.json
|
||||
```
|
||||
|
||||
Pierwszym repozytorium w `tools/` jest `stem-launcher`; to ono pobiera
|
||||
workspace-info, źródła kart oraz repo środowiska. Pozostałe repozytoria są
|
||||
zarządzane przez `stemctl`, a nie klonowane wewnątrz kontenera.
|
||||
|
||||
Karta jest montowana jako całe `/workspace`, łącznie z przypiętymi źródłami
|
||||
symulatora i trwałymi artefaktami:
|
||||
|
||||
```text
|
||||
~/dev/workspace/stem/series/<seria>/<karta>/
|
||||
├── src/
|
||||
├── vendor/Hazard3/
|
||||
├── vendor/lab-runtime/
|
||||
└── .stem/instances/.../build/
|
||||
```
|
||||
|
||||
Przygotowanie i kontrola bez uruchamiania kontenera:
|
||||
|
||||
```bash
|
||||
./stemctl env sources inf pointers
|
||||
./stemctl env sources inf pointers --check
|
||||
```
|
||||
|
||||
Każda zwykła akcja `build/test/run/debug` wykonuje przygotowanie automatycznie.
|
||||
Launcher nie klonuje źródeł w kontenerze, nie tworzy symlinków do `/opt` i nie
|
||||
nadpisuje zmodyfikowanego katalogu `vendor/`. Przypięcie pochodzi z
|
||||
`sources.lock.json` repo środowiska.
|
||||
|
||||
Jeżeli istnieje tylko starszy `~/dev/workspace/rv`, launcher wykrywa go bez
|
||||
niszczenia danych. Jawna migracja:
|
||||
|
||||
@@ -101,6 +163,30 @@ się w wybranym kontenerze:
|
||||
Oddzielne wpisy klienta MCP mogą wskazywać inne nazwy instancji, więc jeden
|
||||
Codex obsługuje kilka kontenerów bez globalnego „ostatniego socketu”.
|
||||
|
||||
Klient MCP działający stale na hoście może także przełączać oba narzędzia
|
||||
atomowo przez wspólny wskaźnik `current`:
|
||||
|
||||
```bash
|
||||
./stemctl mcp list
|
||||
./stemctl mcp select rp2350-pointers-final
|
||||
./stemctl mcp status
|
||||
./stemctl mcp select 3aca2c1c4c7a
|
||||
```
|
||||
|
||||
Selektor przyjmuje nazwę instancji, nazwę kontenera albo co najmniej 12 znaków
|
||||
ID. Weryfikuje ID i label przez `podman inspect`, a także aktywnie sprawdza oba
|
||||
serwery. Dopiero wtedy atomowo przełącza:
|
||||
|
||||
```text
|
||||
$XDG_RUNTIME_DIR/stem/mcp-selected/current/n.sock
|
||||
$XDG_RUNTIME_DIR/stem/mcp-selected/current/t.sock
|
||||
```
|
||||
|
||||
Wskaźniki prowadzą do katalogu zawierającego aktualny `container12`, więc
|
||||
odtworzony kontener nie może przejąć socketów poprzednika. Neovim i tmux
|
||||
otwierają nowe połączenie przy każdym wywołaniu narzędzia MCP, dlatego zmiana
|
||||
działa bez restartowania klienta.
|
||||
|
||||
## Komputer zdalny
|
||||
|
||||
Na komputer ucznia wchodzimy wyłącznie SSH z parą kluczy. Launcher, Git,
|
||||
@@ -133,6 +219,8 @@ Nowe materiały powinny używać nazw kanonicznych.
|
||||
|
||||
- [plan architektury](doc/architecture-plan.md)
|
||||
- [kontenery i interfejs](doc/containers.md)
|
||||
- [wytyczne dokumentowania debugowania](doc/debug-documentation-guidelines.md)
|
||||
- [sesja karty, UML, MCP i tmux z CLI](doc/session.md)
|
||||
- [migracja nazw i workspace](doc/migration-stem-launcher.md)
|
||||
- [serie i karty](doc/series.md)
|
||||
- [tokeny Gitea](doc/tokens.md)
|
||||
|
||||
@@ -17,6 +17,10 @@ mieć wgląd w tę samą sesję, w której pracuje uczeń:
|
||||
|
||||
Dzięki temu agent widzi środowisko debugowania, a nie tylko statyczne pliki.
|
||||
|
||||
Punkt wejścia `stemctl` jest także projektem hostowym: pierwszy klon znajduje
|
||||
się w `~/dev/workspace/stem/tools/stem-launcher`. Agent ani użytkownik nie
|
||||
klonują launchera, kart lub repozytoriów odpowiedzi do kontenera.
|
||||
|
||||
## Aktualny fundament
|
||||
|
||||
`rv32i-hazard3-student-env` dostarcza źródła wspólnego modelu:
|
||||
@@ -108,6 +112,91 @@ Aktualny ID uniemożliwia użycie socketu pozostałego po odtworzeniu kontenera.
|
||||
`stemctl` traktuje te pliki jako szczegóły implementacyjne. Użytkownik i agent
|
||||
dostają komendy wyższego poziomu.
|
||||
|
||||
### Kto tworzy sockety
|
||||
|
||||
Hostowy launcher `stem` najpierw tworzy katalog runtime i przekazuje go
|
||||
Podmanowi jako bind-mount pod **tą samą bezwzględną ścieżką**. Po uruchomieniu
|
||||
interfejsu debuggera procesy wewnątrz kontenera tworzą sockety:
|
||||
|
||||
- `tmux` tworzy `t.sock`,
|
||||
- `nvim` tworzy `n.sock`.
|
||||
|
||||
Nie są one kopiowane ani przekazywane przez sieć: host i kontener widzą ten sam
|
||||
plik Unix socket w zamontowanym katalogu. ID kontenera jest częścią ścieżki,
|
||||
więc nowy kontener po `rm/start` dostaje nowy katalog i nie może przypadkiem
|
||||
obsłużyć socketu poprzednika.
|
||||
|
||||
### Dynamiczny wybór kontenera
|
||||
|
||||
Stały provider MCP na hoście używa dwóch krótkich ścieżek:
|
||||
|
||||
```text
|
||||
$XDG_RUNTIME_DIR/stem/mcp-selected/current/n.sock
|
||||
$XDG_RUNTIME_DIR/stem/mcp-selected/current/t.sock
|
||||
```
|
||||
|
||||
Polecenie `stemctl mcp select INSTANCE_OR_CONTAINER_ID` weryfikuje registry,
|
||||
pełny ID i label kontenera oraz oba żywe serwery. Następnie atomowo przełącza
|
||||
symlink `current` dla obu socketów. Dzięki temu kolejna operacja MCP trafia do
|
||||
wybranej sesji tmuxa i Neovima bez restartowania Codexa. `stemctl mcp list`
|
||||
pokazuje tylko rekordy registry, a `stemctl mcp status` potwierdza bieżący
|
||||
wybór.
|
||||
|
||||
Wybór pozostaje jawny: samo stworzenie kontenera tworzy jego katalog socketów,
|
||||
ale nie przejmuje automatycznie aktywnego MCP innej sesji.
|
||||
|
||||
### Wiele kont użytkowników i pomoc nauczyciela
|
||||
|
||||
Sockety muszą pozostać prywatne dla unixowego konta, które uruchamia
|
||||
kontener, np.:
|
||||
|
||||
```text
|
||||
/run/user/<uid-ucznia>/stem/<thread>/<instance>/<container-id>/
|
||||
```
|
||||
|
||||
Nie montujemy ich do wspólnego `/tmp`, nie zmieniamy grup socketów tmuxa i nie
|
||||
udostępniamy ich przez port sieciowy. Codex nauczyciela łączy się przez SSH na
|
||||
konkretne konto ucznia; po drugiej stronie ograniczony wrapper MCP łączy się
|
||||
lokalnie z jego `n.sock` i `t.sock`. Stdio SSH jest transportem MCP — socket
|
||||
Unix nie opuszcza komputera ucznia.
|
||||
|
||||
Każdy uczeń ma własny tmux i Neovim. Można uruchomić wiele serwerów MCP dla
|
||||
tej samej sesji (np. ucznia i nauczyciela), ale oba mogą równocześnie edytować
|
||||
bufor lub wysyłać klawisze, więc nie ma gwarancji arbitrażu zmian.
|
||||
|
||||
Dostęp nauczyciela rozdzielamy na dwa klucze SSH:
|
||||
|
||||
- zwykły klucz nauczycielski z pełną powłoką, używany wyłącznie do świadomej,
|
||||
ręcznej interwencji na koncie ucznia;
|
||||
- osobny klucz automatyzacji Codexa z forced command, bez TTY, forwardingu i
|
||||
powłoki, ograniczony do `nvim`, `tmux`, `status` i dozwolonych kontenerów.
|
||||
|
||||
Pełny klucz nauczyciela nie trafia do kontenera ani do konfiguracji MCP.
|
||||
|
||||
### Poziomy uprawnień MCP
|
||||
|
||||
Każdy wpis MCP otrzymuje jawny poziom dostępu. Poziom jest własnością klucza
|
||||
SSH i wrappera po stronie konta ucznia, a nie ustawieniem przekazywanym przez
|
||||
model lub klienta MCP:
|
||||
|
||||
| Poziom | Przeznaczenie | Dozwolone działania |
|
||||
| --- | --- | --- |
|
||||
| `observe` | podgląd postępów | stan nvim, lista i capture pane’ów tmuxa, logi i metadane; bez edycji i wysyłania klawiszy |
|
||||
| `assist` | wspólne rozwiązywanie problemu | działania debuggera i jawnie dozwolona edycja/panele; bez ogólnego terminala oraz bez poleceń powłoki |
|
||||
| `full` | interwencja nauczyciela | pełne sterowanie nvimem i tmuxem, w tym terminalem w wybranym kontenerze, jako konto ucznia |
|
||||
|
||||
`full` jest równoważny interaktywnej pracy na koncie ucznia w granicach
|
||||
wybranego kontenera. W szczególności arbitralne `nvim-remote-expr`, `nvim-ex`
|
||||
lub wysyłanie poleceń do pane’a tmuxa mogą uruchomić kod. Taki wpis tworzymy
|
||||
wyłącznie dla nauczyciela i zapisujemy w audycie konto, instancję, container ID,
|
||||
czas oraz użyty poziom.
|
||||
|
||||
Nie wystarczy przekazać `MCP_ACCESS_LEVEL=observe` do tego samego pełnego
|
||||
serwera: niższe poziomy wymagają osobnych providerów z allowlistą narzędzi.
|
||||
W przeciwnym razie użytkownik nadal mógłby użyć ogólnego Ex/Vimscriptu albo
|
||||
terminala do obejścia ograniczenia. Klucz `full` może korzystać z obecnego
|
||||
providera STEM, ponieważ jego możliwości są celowo pełne.
|
||||
|
||||
## Role profili
|
||||
|
||||
Profil `hazard3-sim`:
|
||||
|
||||
@@ -195,6 +195,38 @@ re-enumeracji. Nie przekazuje całego `/dev`. Build i test offline nie wymagają
|
||||
artefaktu. Bez niego odmawia wykonania, chyba że użytkownik jawnie zaakceptuje
|
||||
istniejący firmware. Zapobiega to testowaniu starego programu.
|
||||
|
||||
## Źródła projektu
|
||||
|
||||
Źródła kart i projektów pobiera wyłącznie `stemctl` na hoście, zawsze pod
|
||||
`~/dev/workspace` danego konta Unix. Kanoniczny workspace STEM ma postać
|
||||
`~/dev/workspace/stem`; nie używamy do pracy katalogów kontenera ani
|
||||
przypadkowych klonów poza `~/dev/workspace`. Pierwszy klon launchera trafia do
|
||||
`~/dev/workspace/stem/tools/stem-launcher` i od niego zaczyna się cały
|
||||
bootstrap workspace. Typowy przepływ to:
|
||||
|
||||
```bash
|
||||
stemctl workspace sync
|
||||
stemctl series cards fetch inf pointers
|
||||
stemctl debug rp2350 inf pointers 4
|
||||
```
|
||||
|
||||
Katalog karty pozostaje na hoście, na przykład
|
||||
`~/dev/workspace/stem/series/inf/pointers/`, i jest przekazywany kontenerowi
|
||||
jako bind-mount `/workspace`. Nie klonujemy repozytoriów źródłowych wewnątrz
|
||||
kontenera: kontenery są odtwarzalne i mogą zostać usunięte, natomiast host
|
||||
zachowuje Git, zmiany ucznia, odpowiedzi i historię pracy. W obrazie lub
|
||||
osobnym cache volume mogą znajdować się wyłącznie narzędzia, zależności i
|
||||
odtwarzalne cache budowania.
|
||||
|
||||
Dla Hazard3 po przygotowaniu przez `stemctl env sources SERIES CARD` ten sam
|
||||
bind-mount zawiera również rzeczywiste (niebędące symlinkami) katalogi
|
||||
`vendor/Hazard3` i `vendor/lab-runtime`. Program, testbench oraz GDB korzystają
|
||||
wyłącznie z nich. Build testbencha i ELF trafia do
|
||||
`.stem/instances/<instance>/build`, a DWARF zachowuje ścieżki
|
||||
`/workspace/vendor/Hazard3/...` i `/workspace/src/...`. `/opt` pozostaje
|
||||
miejscem toolchainów i programów obrazu; nie jest źródłem kodu wyświetlanego w
|
||||
Neovimie ani GDB.
|
||||
|
||||
## Instancje i sockety MCP
|
||||
|
||||
Stabilna nazwa logiczna:
|
||||
@@ -227,6 +259,33 @@ Publiczne wejścia launchera to `stemctl mcp tmux INSTANCE` oraz
|
||||
zweryfikowanego kontenera. Neovim MCP publikuje również stan i komendy
|
||||
Termdebug/nvim-dap.
|
||||
|
||||
Dla jednego stale działającego klienta hostowego dostępny jest również jawny,
|
||||
atomowy przełącznik obu socketów:
|
||||
|
||||
```bash
|
||||
stemctl mcp list
|
||||
stemctl mcp select INSTANCE_OR_CONTAINER_ID
|
||||
stemctl mcp status
|
||||
```
|
||||
|
||||
`mcp select` sprawdza registry, pełny ID i label kontenera, aktywnie testuje
|
||||
serwery Neovima i tmuxa, a następnie jednym `rename(2)` przełącza symlink
|
||||
`$XDG_RUNTIME_DIR/stem/mcp-selected/current`. Jeśli choć jeden serwer nie
|
||||
odpowiada, poprzedni wybór pozostaje bez zmian.
|
||||
|
||||
Katalog socketów jest tworzony przez launcher na hoście i bind-mountowany do
|
||||
kontenera pod identyczną ścieżką. Dopiero `nvim` i `tmux` działające w
|
||||
kontenerze tworzą odpowiednio `n.sock` i `t.sock`; hostowy Codex łączy się z
|
||||
tym samym plikiem przez widok hosta. Sockety nie są przekazywane przez sieć ani
|
||||
nie dają kontenerowi dostępu do socketu Podmana.
|
||||
|
||||
Połączenia MCP mają profil `observe`, `assist` albo `full`. `full` jest
|
||||
przeznaczony dla nauczyciela: może wykonywać dowolne działania dostępne w
|
||||
Neovimie, tmuxie i terminalu wybranego kontenera jako konto ucznia. Profile
|
||||
`observe` i `assist` muszą używać oddzielnych, ograniczonych providerów;
|
||||
nie wolno udawać ograniczenia przez samą zmienną środowiskową przy providerze
|
||||
udostępniającym ogólny Vimscript lub polecenia pane’a.
|
||||
|
||||
MCP nie jest czwartym kontenerem. Bridge jest częścią `dev-ui-base`, a hostowy
|
||||
resolver jest częścią `stem-launcher`.
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Wytyczne dokumentowania debugowania
|
||||
|
||||
## Cel
|
||||
|
||||
Zrzut debuggera jest dowodem pomiaru, nie dekoracją. Ma pozwolić uczniowi
|
||||
połączyć jedną operację w C z instrukcjami RISC-V, rejestrami, ramką stosu i
|
||||
konkretnymi bajtami w RAM.
|
||||
|
||||
## Kadr dowodowy
|
||||
|
||||
- Używaj rzeczywistej sesji `stemctl debug` z karty, symulatora albo płytki.
|
||||
- Zatrzymaj program w jednym nazwanym punkcie: breakpoint, checkpoint albo
|
||||
instrukcja bezpośrednio przed obserwowaną operacją.
|
||||
- Zachowaj wspólny układ: dashboard Termdebug/GDB po lewej, źródło C po
|
||||
prawej, listing `.lst` pod źródłem. Listing musi być zsynchronizowany z PC.
|
||||
- Pokaż tylko dane potrzebne do tezy: wywołanie w C, odpowiadającą instrukcję,
|
||||
argument w rejestrze, fragment stosu albo pamięć w RAM.
|
||||
- Dołącz podpis z platformą, taskiem i stanem pomiaru, na przykład „przed
|
||||
pierwszym przydziałem pamięci”.
|
||||
|
||||
## Oznaczenia
|
||||
|
||||
- Stosuj najwyżej cztery krótkie oznaczenia numeryczne na jednym kadrze.
|
||||
- Każdy numer ma odpowiadać jednemu zdaniu w podpisie lub legendzie.
|
||||
- Bieżący kolor oznaczeń to czerwony: oznacza „zatrzymaj się i sprawdź”.
|
||||
Paleta może później ulec zmianie, ale numeracja i podpis muszą pozostać
|
||||
zrozumiałe bez koloru.
|
||||
- Nie zasłaniaj kodu ani nie zmieniaj jego treści. Zachowaj surowy zrzut jako
|
||||
źródło, a adnotowany obraz zapisz jako osobny plik.
|
||||
|
||||
## Wstawienie do karty
|
||||
|
||||
- Zasoby zapisuj w `doc/assets/`, np.
|
||||
`task04-first-allocation-annotated.png`.
|
||||
- W `doc/main.tex` umieść obraz blisko instrukcji, której dotyczy, oraz dodaj
|
||||
zwięzły podpis wyjaśniający numery.
|
||||
- Po zmianie zbuduj PDF skryptem `scripts/render_pdf.sh` i sprawdź stronę
|
||||
wynikową w rozmiarze A4.
|
||||
- W materiałach dla ucznia używaj słowa **RAM**, nie skrótu „SRAM”.
|
||||
|
||||
## Przykład referencyjny
|
||||
|
||||
Karta `inf/pointers`, Task04: `alloc_local(5)` przed pierwszym przydziałem.
|
||||
Kadr pokazuje wywołanie C, `li a0,5` i `jal alloc_local`, wartość `a0=5` oraz
|
||||
pusty `allocbuf`. Taki obraz dokumentuje związek źródła, ABI i pamięci bez
|
||||
zastępowania go opisem narracyjnym.
|
||||
@@ -1,7 +1,6 @@
|
||||
# Migracja `rv-launcher` do `stem-launcher`
|
||||
|
||||
Status: M1–M3 wdrożone lokalnie; M4 (zmiana nazwy zdalnego repo) celowo
|
||||
odłożone do zakończenia okresu zgodności
|
||||
Status: M1–M4 wdrożone; M5 pozostaje okresem zgodności
|
||||
Data: 2026-07-14
|
||||
|
||||
## Dlaczego zmieniamy nazwę
|
||||
@@ -34,10 +33,10 @@ na serwerze. Odwrotna kolejność zepsułaby bootstrap, token records, remotes,
|
||||
### M1 — alias CLI (wdrożone)
|
||||
|
||||
- dodać wykonywalny `stemctl` wskazujący tę samą implementację;
|
||||
- pozostawić `rvctl` jako wrapper;
|
||||
- pozostawić `rvctl` jako cichy wrapper;
|
||||
- pomoc i nowe materiały pokazują wyłącznie `stemctl`;
|
||||
- `rvctl` drukuje jednorazowe ostrzeżenie o wycofaniu na stderr, ale zachowuje
|
||||
format stdout potrzebny skryptom.
|
||||
- `rvctl` nie dopisuje ostrzeżeń do stderr i zachowuje format stdout/stderr
|
||||
potrzebny istniejącym skryptom.
|
||||
|
||||
### M2 — konfiguracja i ścieżki (wdrożone)
|
||||
|
||||
@@ -59,19 +58,19 @@ nie usuwa starego katalogu przed zweryfikowaniem nowego.
|
||||
- kontenery otrzymują labels z nazwą kanoniczną, nie aliasem;
|
||||
- wyniki zapisują nazwę kanoniczną oraz opcjonalne `requested_alias`.
|
||||
|
||||
### M4 — repo Gitea (odłożone)
|
||||
### M4 — repo Gitea (wdrożone 2026-07-14)
|
||||
|
||||
Po wydaniu kompatybilnego launchera:
|
||||
Po wydaniu kompatybilnego launchera wykonano:
|
||||
|
||||
1. utworzyć lub zmienić nazwę na `edu-tools/stem-launcher`;
|
||||
2. zaktualizować `workspace-info`, bootstrap i token records;
|
||||
3. sprawdzić clone/fetch/push przez nowe URL;
|
||||
4. pozostawić pod `edu-tools/rv-launcher` przekierowanie albo małe repo z
|
||||
komunikatem migracyjnym, zależnie od możliwości Gitea;
|
||||
5. nie usuwać starej nazwy podczas trwającego semestru.
|
||||
1. zmianę nazwy na `edu-tools/stem-launcher` przez API Gitea;
|
||||
2. aktualizację lokalnego `origin`, bootstrapu, dokumentacji i przykładów
|
||||
token records;
|
||||
3. weryfikację clone/fetch/push przez nowy URL;
|
||||
4. weryfikację przekierowania starej nazwy przez Gitea;
|
||||
5. zachowanie wrappera `rvctl` i pozostałych aliasów na okres zgodności.
|
||||
|
||||
Zmiana repo na Gitea jest operacją administracyjną i nie jest wykonywana przez
|
||||
samą aktualizację dokumentacji.
|
||||
Operację administracyjną wykonano po SSH do VPS i przez ograniczony token API;
|
||||
sekret nie znajduje się w repo ani w konfiguracji launchera.
|
||||
|
||||
### M5 — wycofanie kompatybilności
|
||||
|
||||
|
||||
+14
-14
@@ -19,11 +19,11 @@ Workspace służy do klonów roboczych i ćwiczeń:
|
||||
Typowy układ:
|
||||
|
||||
```text
|
||||
~/dev/edu/repos/rv/rv-launcher
|
||||
~/dev/edu/repos/stem/stem-launcher
|
||||
~/dev/edu/repos/rv/rv32i-hazard3-env
|
||||
~/dev/edu/repos/rv/series/<seria>/<karta>
|
||||
~/dev/workspace/rv/meta/workspace-info
|
||||
~/dev/workspace/rv/tools/rv-launcher
|
||||
~/dev/workspace/stem/tools/stem-launcher
|
||||
~/dev/workspace/rv/tools/rv32i-hazard3-env
|
||||
~/dev/workspace/rv/series/<seria>/<karta>
|
||||
```
|
||||
@@ -46,7 +46,7 @@ Komendy launchera pracują na `series_root`, czyli na klonach roboczych.
|
||||
Repo treningowe launchera trzymaj pod:
|
||||
|
||||
```bash
|
||||
~/dev/workspace/rv/tools/rv-launcher
|
||||
~/dev/workspace/stem/tools/stem-launcher
|
||||
```
|
||||
|
||||
Podstawowy bootstrap wygląda tak:
|
||||
@@ -54,8 +54,8 @@ Podstawowy bootstrap wygląda tak:
|
||||
```bash
|
||||
mkdir -p ~/dev/workspace/rv/tools
|
||||
cd ~/dev/workspace/rv/tools
|
||||
git clone http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
||||
cd rv-launcher
|
||||
git clone http://77.90.8.171:3001/edu-tools/stem-launcher.git
|
||||
cd stem-launcher
|
||||
```
|
||||
|
||||
Główne pliki CLI:
|
||||
@@ -89,7 +89,7 @@ tokenem do zdalnego endpointu. Jeśli launcher zobaczy URL w formacie
|
||||
Przykład:
|
||||
|
||||
```bash
|
||||
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git
|
||||
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/stem-launcher.git
|
||||
git fetch r1 main
|
||||
git switch --track -c main r1/main
|
||||
```
|
||||
@@ -120,7 +120,7 @@ Minimalny format pliku:
|
||||
},
|
||||
"user": "u1",
|
||||
"org": "edu-tools",
|
||||
"repo": "rv-launcher"
|
||||
"repo": "stem-launcher"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -757,7 +757,7 @@ Typowy wynik:
|
||||
remotes
|
||||
item remote kind server proto host org repo user token result url
|
||||
---- ------ ----------- ------ ----- ------------------ --------- ----------- ---- ------------ -------------- --------------------------------------
|
||||
1 r1 auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 e59cc...13be found http://77.90.8.171:3001/edu-tools/...
|
||||
1 r1 auth gitea http 77.90.8.171:3001 edu-tools stem-launcher u1 e59cc...13be found http://77.90.8.171:3001/edu-tools/...
|
||||
```
|
||||
|
||||
Przykład:
|
||||
@@ -784,7 +784,7 @@ Typowy wynik:
|
||||
tokens
|
||||
item server proto host org repo user remote token_ref token valid scope org repo
|
||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 * e59cc...13be forever wwwwwwwww +++++ ++++
|
||||
1 gitea http 77.90.8.171:3001 edu-tools stem-launcher u1 r1 r1 * e59cc...13be forever wwwwwwwww +++++ ++++
|
||||
```
|
||||
|
||||
`token_ref` jest komórką stałej szerokości: nazwa tokena jest po lewej, a marker
|
||||
@@ -827,8 +827,8 @@ Typowy wynik:
|
||||
tokens
|
||||
item source kind server proto host org repo user remote token valid scope org repo
|
||||
---- ------ ----- ------ ----- ------------------ --------- ----------- ---- ------ ------------ ------------------- aAimnopru oawrc- oawr--
|
||||
1 store auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 e59cc...13be forever --------- +++++ ++++
|
||||
2 remote auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 e59cc...13be
|
||||
1 store auth gitea http 77.90.8.171:3001 edu-tools stem-launcher u1 r1 e59cc...13be forever --------- +++++ ++++
|
||||
2 remote auth gitea http 77.90.8.171:3001 edu-tools stem-launcher u1 r1 e59cc...13be
|
||||
```
|
||||
|
||||
Przykłady:
|
||||
@@ -866,7 +866,7 @@ Przykłady:
|
||||
|
||||
```bash
|
||||
./rvctl tokens add r1
|
||||
./rvctl tokens add r1 --user u1 --org edu-tools --repo rv-launcher
|
||||
./rvctl tokens add r1 --user u1 --org edu-tools --repo stem-launcher
|
||||
```
|
||||
|
||||
## `tokens sync remote REMOTE_ID`
|
||||
@@ -885,7 +885,7 @@ Przykład:
|
||||
|
||||
```bash
|
||||
./rvctl tokens sync remote r1
|
||||
./rvctl tokens sync remote r1 --repo ~/dev/workspace/rv/tools/rv-launcher
|
||||
./rvctl tokens sync remote r1 --repo ~/dev/workspace/stem/tools/stem-launcher
|
||||
```
|
||||
|
||||
## `tokens sync store REMOTE_ID`
|
||||
@@ -996,7 +996,7 @@ token_path<TAB>...
|
||||
tokens
|
||||
item server proto host org repo user remote token_ref token valid scope org repo
|
||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 * e59cc...13be forever -----w--- +++++ ++++
|
||||
1 gitea http 77.90.8.171:3001 edu-tools stem-launcher u1 r1 r1 * e59cc...13be forever -----w--- +++++ ++++
|
||||
|
||||
status
|
||||
item<TAB>value
|
||||
|
||||
+26
-19
@@ -11,24 +11,30 @@ Komendy kart pracy dzielimy na trzy poziomy:
|
||||
- `series cards` - operacje na kartach w ramach serii
|
||||
- `series cards tasks` - operacje na zadaniach w ramach karty
|
||||
|
||||
`rvctl` tworzy `series_root`, jeżeli katalog jeszcze nie istnieje. Lista serii
|
||||
i kart pochodzi docelowo z publicznego manifestu
|
||||
`~/dev/workspace/rv/meta/workspace-info/workspace.json`. Jeżeli manifestu nie
|
||||
ma, komendy zależne od serii automatycznie klonują repo
|
||||
`edu-workspace/workspace-info`. Jeżeli klonowanie nie jest możliwe, narzędzie
|
||||
używa starego fallbacku przez `original_series_root`.
|
||||
`stemctl` tworzy `series_root`, jeżeli katalog jeszcze nie istnieje. Gdy
|
||||
`workspace-info` jest dostępny, manifest jest jedynym źródłem listy serii i
|
||||
kart. Przypadkowe katalogi robocze nie pojawiają się w katalogu. Fallback przez
|
||||
`original_series_root` działa wyłącznie dla starego workspace bez manifestu.
|
||||
|
||||
Każda operacyjna seria jawnie deklaruje `source_org`. Docelowa konwencja to
|
||||
`edu-<series-id>`, na przykład `freertos-c` → `edu-freertos-c`. Jedno repo
|
||||
odpowiada jednej karcie; kolejnych wydań karty nie zapisujemy jako osobnych
|
||||
repozytoriów ani trwałych branchy, tylko jako historię, tagi i wydania tego
|
||||
repozytorium. Organizacja `edu` przechowuje control plane i katalog, nie setki
|
||||
repozytoriów kart.
|
||||
|
||||
## Szybki przepływ
|
||||
|
||||
```bash
|
||||
./rvctl tokens compare
|
||||
./rvctl series list
|
||||
./rvctl series use inf
|
||||
./rvctl series cards list inf
|
||||
./rvctl card use bss
|
||||
./rvctl series cards fetch inf bss
|
||||
./rvctl tasks list
|
||||
./rvctl tasks switch 1
|
||||
./stemctl workspace audit
|
||||
./stemctl tokens compare
|
||||
./stemctl series list
|
||||
./stemctl series use freertos-c
|
||||
./stemctl series cards list freertos-c
|
||||
./stemctl card use FC02
|
||||
./stemctl series cards fetch freertos-c FC02
|
||||
./stemctl tasks list
|
||||
./stemctl tasks switch 1
|
||||
```
|
||||
|
||||
Dla karty `bss` właściwym repo jest `lab-rv32i-strlen-bss-data-stack`.
|
||||
@@ -37,10 +43,10 @@ Dla karty `bss` właściwym repo jest `lab-rv32i-strlen-bss-data-stack`.
|
||||
|
||||
### `series list`
|
||||
|
||||
Listuje dostępne serie z manifestu `workspace-info` oraz liczbę kart już
|
||||
pobranych do lokalnego workspace. Jeżeli manifestu jeszcze nie ma, komenda
|
||||
pobiera go automatycznie. Jeżeli `~/dev/workspace/rv/series` nie istnieje,
|
||||
komenda tworzy ten katalog.
|
||||
Listuje wyłącznie operacyjne serie z manifestu `workspace-info` oraz liczbę
|
||||
kart już obecnych w lokalnym workspace. Logiczna seria może wskazywać wspólny,
|
||||
przejściowy katalog fizyczny przez `workspace_dir`; na przykład `freertos-c`
|
||||
jest obecnie mapowane na `series/freertos`.
|
||||
|
||||
```bash
|
||||
./rvctl series list
|
||||
@@ -50,7 +56,8 @@ Typowy wynik:
|
||||
|
||||
```text
|
||||
fiz 3 0
|
||||
inf 3 0
|
||||
inf 9 5
|
||||
freertos-c 11 11
|
||||
```
|
||||
|
||||
Kolumny oznaczają: seria, liczba kart w źródłach, liczba kart pobranych do
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
# Sesja karty z wiersza poleceń
|
||||
|
||||
`stemctl session` łączy pięć selektorów w jeden kontrakt:
|
||||
|
||||
```text
|
||||
seria → karta → Task → blok/faza/krok/snapshot UML → instancja kontenera
|
||||
```
|
||||
|
||||
Każdy selektor przyjmuje stabilne ID. Seria, karta, Task, blok, faza, krok i
|
||||
snapshot przyjmują także numery pokazywane przez `choices`. Numer karty jest
|
||||
jednobazową pozycją w manifeście serii, więc nie zależy od tego, czy repo karty
|
||||
zostało już pobrane. W przypadku kroków najpierw sprawdzany jest globalny numer
|
||||
strzałki z diagramu, a przy jawnie wybranej fazie także lokalny numer kroku.
|
||||
|
||||
## Lista wyboru
|
||||
|
||||
```bash
|
||||
./stemctl session choices --series inf --card 7 --task 4
|
||||
./stemctl session choices --series inf --card pointers --json
|
||||
```
|
||||
|
||||
Tabele zawierają kolumnę `status`:
|
||||
|
||||
- `opracowane` — materiał UML ma komplet recept replay/checkpoint;
|
||||
- `robocze` — istnieje źródło albo częściowe metadane;
|
||||
- `brak` — karta lub materiał nie znajduje się jeszcze w workspace.
|
||||
|
||||
## Sterowanie środowiskiem
|
||||
|
||||
```bash
|
||||
# Utwórz debugger Task04 w pane 0 okna, z którego uruchomiono Codexa.
|
||||
./stemctl session start hazard3-sim --series inf --card 7 --task 4
|
||||
|
||||
# Usuń i odtwórz kontener, po czym zatrzymaj maszynę na strzałce 12.
|
||||
./stemctl session reset hazard3-sim \
|
||||
--series inf --card 7 --task 4 --step 12
|
||||
|
||||
# Zachowaj kontener i ponownie podepnij istniejący debugger.
|
||||
./stemctl session refresh hazard3-sim --series inf --card 7 --task 4
|
||||
|
||||
# Podepnij/odepnij kontener od pane zewnętrznego tmuxa. Procesy wewnątrz żyją dalej.
|
||||
./stemctl session attach hazard3-sim --series inf --card 7 --task 4
|
||||
./stemctl session detach hazard3-sim --series inf --card 7 --task 4
|
||||
```
|
||||
|
||||
Domyślny cel `--pane current:0` oznacza pane `0` tego samego okna tmuxa, w
|
||||
którym działa wywołujący Codex. Można podać jawne `%ID`, `sesja:okno.pane` albo
|
||||
`--pane none`. Launcher odmawia zastąpienia pane, z którego sam został
|
||||
uruchomiony, aby nie zakończyć Codexa. Pane Codexa jest dodatkowo oznaczone
|
||||
PID-em pane w opcjach tmuxa; jawne `%ID` podane z innego terminala również
|
||||
zostanie odrzucone. Świadome obejście tej drugiej ochrony wymaga
|
||||
`--force-pane`; nie da się nim zastąpić pane wykonującego bieżącą komendę.
|
||||
|
||||
`attach` i `detach` operują na połączeniu pane z kontenerem. Neovim i
|
||||
Termdebug są zawartością jego sesji. `detach` nie zatrzymuje kontenera,
|
||||
symulatora, GDB, Neovima ani wewnętrznej sesji tmuxa.
|
||||
|
||||
## Wybór stanu UML
|
||||
|
||||
Równoważne przykłady:
|
||||
|
||||
```bash
|
||||
./stemctl session stage hazard3-sim --series inf --card 7 --task 4 --step 12
|
||||
./stemctl session checkpoint hazard3-sim --series inf --card pointers --task 4 \
|
||||
--snapshot task04.alloc5.commit
|
||||
./stemctl session stage hazard3-sim --series inf --card 7 --task 4 --stage alloc-5/7
|
||||
./stemctl session stage hazard3-sim --series inf --card 7 --task 4 \
|
||||
--stage task04/allocator-flow/alloc-5/alloc5-commit
|
||||
./stemctl session stage hazard3-sim --series inf --card 7 --task 4 \
|
||||
--phase 2 --step 3
|
||||
./stemctl session stage hazard3-sim --series inf --card 7 --task 4 --first
|
||||
```
|
||||
|
||||
W ostatnim przykładzie `3` oznacza trzeci krok wewnątrz drugiej fazy, jeżeli
|
||||
w tej fazie nie istnieje globalna strzałka numer 3.
|
||||
|
||||
Tryb online najpierw sprawdza pełną tożsamość karty (`id`, UUID, wersję i hash
|
||||
źródłowego JSON-a), receptę checkpointu, profil, target oraz bind mount
|
||||
`/workspace`. Następnie wybiera sockety MCP i przekazuje kontrolerowi
|
||||
oczekiwane `instance/container_id/profile/target`. Replay zostaje odrzucony,
|
||||
jeżeli globalny wybór MCP zmieni się przed aktywacją albo podczas niej. Dopiero
|
||||
po udanym zatrzymaniu GDB publikowane są pozycja strony i `SYNC ON`.
|
||||
|
||||
`--offline` zmienia wyłącznie stan strony i wyłącza SYNC/sterowanie Neovimem.
|
||||
`--control` dodatkowo uzbraja sterowanie klawiaturą Neovima; samo SYNC nie robi
|
||||
tego automatycznie. `stage/checkpoint` wymaga jawnego selektora; pierwszy etap
|
||||
wybiera się świadomie przez `--first`.
|
||||
|
||||
```bash
|
||||
./stemctl session stage hazard3-sim --series inf --card 7 --task 4 \
|
||||
--step 12 --offline
|
||||
./stemctl session status hazard3-sim --series inf --card 7 --task 4
|
||||
./stemctl session status hazard3-sim --series inf --card 7 --task 4 \
|
||||
--json --strict
|
||||
```
|
||||
|
||||
Adres serwera karty można zmienić przez `--card-url` albo `STEM_CARD_URL`.
|
||||
Limit oczekiwania na Neovima/GDB/MCP ustawia `--timeout`.
|
||||
|
||||
## Przyszłe strategie i adnotacje
|
||||
|
||||
Kluczem rozszerzeń pozostaje pełna pozycja
|
||||
`task/block/phase/step/snapshot`. Pod tym kluczem będzie można później
|
||||
przechowywać wiele strategii debugowania i adnotacji bez zmiany obecnego CLI.
|
||||
Planowany kontrakt rozdziela treść od widoczności:
|
||||
|
||||
```text
|
||||
strategy: id, label, commands, expected_observations
|
||||
annotation: id, strategy_id, target, geometry, style, text, visible
|
||||
```
|
||||
|
||||
`target` może wskazywać diagram, bufor/wiersz Neovima, rejestr, ramkę stosu
|
||||
albo zakres pamięci. Operacje `show/hide/toggle` mają zmieniać widoczność bez
|
||||
usuwania adnotacji; `add/remove` będą osobnymi, audytowalnymi operacjami.
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
# `stemctl` — sterowanie sesją zajęć
|
||||
|
||||
`stemctl session` łączy wybór materiału, kontenera, stanu UML i zewnętrznego
|
||||
pane tmuxa. Szczegółowy opis wyboru checkpointów znajduje się w
|
||||
[`session.md`](session.md); ten dokument definiuje znaczenie poleceń cyklu
|
||||
życia.
|
||||
|
||||
## Model połączenia
|
||||
|
||||
```text
|
||||
pane tmuxa na hoście
|
||||
│
|
||||
│ attach / detach
|
||||
▼
|
||||
wybrany kontener
|
||||
└── wewnętrzny tmux → Neovim + Termdebug/GDB + symulator lub RP2350
|
||||
```
|
||||
|
||||
## Katalog i organizacje Gitea
|
||||
|
||||
`workspace-info/workspace.json` jest źródłem prawdy. Organizacja `edu` pełni
|
||||
rolę control plane, a stabilne serie mają osobne organizacje o nazwie
|
||||
`edu-<series-id>`. Karta jest repozytorium wewnątrz organizacji swojej serii.
|
||||
|
||||
```bash
|
||||
stemctl workspace audit
|
||||
stemctl series list
|
||||
stemctl series cards list freertos-c
|
||||
stemctl series cards show freertos-c FC02
|
||||
```
|
||||
|
||||
`workspace audit` jest lokalny i deterministyczny: sprawdza kontrakt katalogu,
|
||||
nie modyfikuje Gitea. Dzięki temu brak sieci nie uniemożliwia przeprowadzenia
|
||||
zajęć z wcześniej zsynchronizowanego workspace.
|
||||
|
||||
Obiektem operacji `attach` i `detach` jest **połączenie pane z kontenerem**.
|
||||
Neovim i Termdebug są zawartością sesji kontenera, a nie osobnym obiektem
|
||||
podpinanym przez launcher.
|
||||
|
||||
- `attach` podpina wybrany, działający kontener do wskazanego pane i pokazuje
|
||||
jego istniejącą sesję Neovim/Termdebug;
|
||||
- `detach` zastępuje widok kontenera zwykłą powłoką hosta w tym pane;
|
||||
- `detach` nie zatrzymuje kontenera, wewnętrznego tmuxa, Neovima, GDB,
|
||||
symulatora ani połączenia z RP2350;
|
||||
- ponowne `attach` wraca do tej samej działającej sesji;
|
||||
- `stop` i `rm`, a nie `detach`, zmieniają cykl życia kontenera.
|
||||
|
||||
## Polecenia sesji
|
||||
|
||||
| Polecenie | Znaczenie |
|
||||
| --- | --- |
|
||||
| `session choices` | Wyświetla numerowane serie, karty, Taski i etapy UML. |
|
||||
| `session status` | Sprawdza kontener, MCP, tożsamość karty i bieżący etap. |
|
||||
| `session start` | Uruchamia kontener i jego środowisko pracy. |
|
||||
| `session reset` | Usuwa i odtwarza kontener, następnie opcjonalnie odtwarza checkpoint. |
|
||||
| `session refresh` | Zachowuje kontener i odnawia jego podpięcie oraz wybór MCP. |
|
||||
| `session attach` | Podpina istniejący kontener do pane tmuxa na hoście. |
|
||||
| `session detach` | Odpina kontener od pane bez zatrzymywania czegokolwiek wewnątrz. |
|
||||
| `session stage` | Ustawia wybrany etap UML; alias: `session checkpoint`. |
|
||||
|
||||
## Wybór celu
|
||||
|
||||
Serię, kartę, Task i elementy UML można wskazywać stabilnym ID albo numerem
|
||||
pokazanym przez `choices`:
|
||||
|
||||
```bash
|
||||
stemctl session choices --series inf --card 7 --task 4
|
||||
|
||||
stemctl session reset hazard3-sim \
|
||||
--series inf --card 7 --task 4 \
|
||||
--step 12 --pane current:0
|
||||
```
|
||||
|
||||
Najważniejsze selektory:
|
||||
|
||||
- środowisko: `profile`, `--target`, `--instance`;
|
||||
- materiał: `--series`, `--card`, `--task`;
|
||||
- UML: `--stage`, `--block`, `--phase`, `--step`, `--snapshot`, `--first`;
|
||||
- widok: `--pane`, `--focus`, `--offline`, `--control`;
|
||||
- wykonanie: `--card-url`, `--timeout`, `--force-pane`, `--dry-run`.
|
||||
|
||||
`--pane current:0` oznacza pane `0` tego okna tmuxa, w którym uruchomiono
|
||||
Codexa. Można także podać `%ID` albo `sesja:okno.pane`. Launcher chroni pane
|
||||
wykonujące bieżącą komendę i pane oznaczone jako należące do innej sesji
|
||||
Codexa.
|
||||
|
||||
## Przykłady attach i detach
|
||||
|
||||
```bash
|
||||
# Podepnij działający kontener Task04 do pane 0 bieżącego okna.
|
||||
stemctl session attach hazard3-sim \
|
||||
--series inf --card 7 --task 4 --pane current:0
|
||||
|
||||
# Wróć w pane 0 do powłoki hosta. Kontener i debugowanie nadal działają.
|
||||
stemctl session detach hazard3-sim \
|
||||
--series inf --card 7 --task 4 --pane current:0
|
||||
```
|
||||
|
||||
## Status materiału
|
||||
|
||||
`session choices` pokazuje jedną z trzech wartości:
|
||||
|
||||
- `opracowane` — kompletny materiał wraz z receptą checkpointu;
|
||||
- `robocze` — źródło lub metadane istnieją, ale nie są kompletne;
|
||||
- `brak` — materiał nie jest jeszcze dostępny w workspace.
|
||||
+7
-7
@@ -9,7 +9,7 @@ Rekord w `tokens.json` zawiera token, endpoint serwera, login oraz docelowe
|
||||
`org/repo`. Git remote służy tylko do operacji Git (`fetch`, `push`) albo do
|
||||
pierwszego wczytania tokena do store.
|
||||
|
||||
Bez `--repo` komendy tokenów działają na repo `rv-launcher`. Dla kart pracy albo
|
||||
Bez `--repo` komendy tokenów działają na repo `stem-launcher`. Dla kart pracy albo
|
||||
innych repo podaj `--repo PATH`.
|
||||
|
||||
## Szybki przepływ
|
||||
@@ -17,7 +17,7 @@ innych repo podaj `--repo PATH`.
|
||||
Startujemy od git remota z tokenem w URL-u:
|
||||
|
||||
```bash
|
||||
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git
|
||||
git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/stem-launcher.git
|
||||
```
|
||||
|
||||
Sprawdzamy, co jest zapisane w remote i store:
|
||||
@@ -133,7 +133,7 @@ Jeśli remote `r1` nie istnieje, `rvctl` buduje URL z pól `server.endpoint`,
|
||||
`org` i `repo` w `tokens.json`, na przykład:
|
||||
|
||||
```text
|
||||
http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
||||
http://77.90.8.171:3001/edu-tools/stem-launcher.git
|
||||
```
|
||||
|
||||
Jeżeli w repo istnieje tylko `origin` wskazujący ten sam URL, `sync store r1`
|
||||
@@ -142,7 +142,7 @@ automatycznie przemianuje `origin` na `r1`, a potem wpisze credentials.
|
||||
Opcjonalnie można podać URL ręcznie:
|
||||
|
||||
```bash
|
||||
./rvctl tokens sync store r1 --url http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
||||
./rvctl tokens sync store r1 --url http://77.90.8.171:3001/edu-tools/stem-launcher.git
|
||||
```
|
||||
|
||||
Jeśli remote ma już inne credentials, użyj:
|
||||
@@ -265,7 +265,7 @@ domyślnie pochodzi z `workspace.json`.
|
||||
|
||||
```bash
|
||||
./rvctl tokens add r1
|
||||
./rvctl tokens add r1 --server http://77.90.8.171:3001 --user u1 --org edu-tools --repo rv-launcher
|
||||
./rvctl tokens add r1 --server http://77.90.8.171:3001 --user u1 --org edu-tools --repo stem-launcher
|
||||
```
|
||||
|
||||
Najczęściej pusty szkielet ma sens wtedy, gdy chcesz ręcznie wpisać token w
|
||||
@@ -279,7 +279,7 @@ Pokazuje kontekst, tabelę `tokens` i podsumowanie statusów endpointów.
|
||||
|
||||
```bash
|
||||
./rvctl tokens stats
|
||||
./rvctl tokens stats --repo ~/dev/workspace/rv/tools/rv-launcher
|
||||
./rvctl tokens stats --repo ~/dev/workspace/stem/tools/stem-launcher
|
||||
```
|
||||
|
||||
## Uprawnienia
|
||||
@@ -384,7 +384,7 @@ Minimalny przykład:
|
||||
},
|
||||
"user": "u1",
|
||||
"org": "edu-tools",
|
||||
"repo": "rv-launcher"
|
||||
"repo": "stem-launcher"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://edu-tools.local/rv-launcher/tokens.schema.json",
|
||||
"title": "RV launcher tokens",
|
||||
"$id": "https://edu-tools.local/stem-launcher/tokens.schema.json",
|
||||
"title": "STEM launcher tokens",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "tokens"],
|
||||
|
||||
+31
-7
@@ -13,33 +13,57 @@ Implementacja CLI znajduje się w `rvctl.py`. Plik `workspace.json` przechowuje
|
||||
lokalne ścieżki workspace, series, socketów, tokenów i adres publicznego
|
||||
manifestu.
|
||||
|
||||
Publiczny manifest wspólnego workspace jest w repo:
|
||||
Publiczny manifest wspólnego workspace jest częścią control plane w
|
||||
organizacji `edu`. Katalog jest źródłem prawdy dla narzędzi i interfejsu;
|
||||
lista organizacji widoczna w Gitea pozostaje widokiem administracyjnym.
|
||||
|
||||
Model własności:
|
||||
|
||||
```text
|
||||
edu-workspace/workspace-info
|
||||
edu
|
||||
└── workspace-info control plane i katalog
|
||||
|
||||
edu-inf seria legacy
|
||||
edu-fiz seria
|
||||
edu-freertos-c seria
|
||||
├── lab-rv32i-freertos-heap4 jedna karta = jedno repo
|
||||
├── lab-rv32i-freertos-c-first-task
|
||||
└── ...
|
||||
```
|
||||
|
||||
Lokalna kopia manifestu znajduje się w:
|
||||
|
||||
```text
|
||||
~/dev/workspace/rv/meta/workspace-info
|
||||
~/dev/workspace/stem/meta/workspace-info
|
||||
```
|
||||
|
||||
Typowy układ:
|
||||
|
||||
```text
|
||||
~/dev/workspace/rv
|
||||
~/dev/workspace/stem
|
||||
├── meta
|
||||
│ └── workspace-info
|
||||
├── tools
|
||||
│ └── rv-launcher
|
||||
│ └── stem-launcher
|
||||
├── tokens
|
||||
│ └── tokens.json
|
||||
└── series
|
||||
```
|
||||
|
||||
`workspace-info` opisuje serie, karty, repo źródłowe, repo odpowiedzi i branche.
|
||||
Nie przechowuje tokenów ani lokalnych plików roboczych ucznia.
|
||||
`workspace-info` opisuje rejestr organizacji, operacyjne serie, karty, repo
|
||||
źródłowe, repo odpowiedzi i branche. Każda seria musi jawnie podać
|
||||
`source_org`; globalne `git.source_org` nie zastępuje tej deklaracji.
|
||||
`workspace-info` nie przechowuje tokenów ani lokalnych plików roboczych ucznia.
|
||||
|
||||
Sprawdzenie spójności bez połączenia z Gitea:
|
||||
|
||||
```bash
|
||||
./stemctl workspace audit
|
||||
```
|
||||
|
||||
Audyt wykrywa brak organizacji, brak jawnego `source_org`, niespójne
|
||||
przypisanie seria–organizacja oraz zduplikowane identyfikatory kart i repo.
|
||||
Ostrzeżenie o wspólnym `workspace_dir` jest dopuszczalne podczas migracji.
|
||||
|
||||
Aktualizacja manifestu:
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [[ "${STEM_RVCTL_SILENCE_DEPRECATION:-0}" != "1" ]]; then
|
||||
printf '%s\n' 'warning: rvctl is deprecated; use stemctl (compatibility mode remains enabled).' >&2
|
||||
fi
|
||||
export STEM_CLI_NAME=rvctl
|
||||
exec python3 "$SCRIPT_DIR/rvctl.py" "$@"
|
||||
|
||||
+503
-2
@@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import json
|
||||
import io
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
@@ -18,9 +19,21 @@ import rvctl # noqa: E402
|
||||
|
||||
|
||||
class StemctlContractTests(unittest.TestCase):
|
||||
def test_rvctl_compatibility_wrapper_is_silent(self) -> None:
|
||||
wrapper = Path(__file__).resolve().parents[1] / "rvctl"
|
||||
completed = subprocess.run(
|
||||
[str(wrapper), "--help"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
self.assertEqual(completed.returncode, 0, completed.stderr)
|
||||
self.assertNotIn("deprecated", completed.stderr.lower())
|
||||
self.assertNotIn("warning", completed.stderr.lower())
|
||||
|
||||
def test_new_command_help_never_crashes(self) -> None:
|
||||
script = Path(__file__).resolve().parents[1] / "rvctl.py"
|
||||
commands = ["env", "build", "test", "run", "debug", "deploy", "shell", "start", "status", "attach", "stop", "rm", "probe", "mcp"]
|
||||
commands = ["env", "build", "test", "run", "debug", "deploy", "shell", "start", "status", "attach", "stop", "rm", "probe", "mcp", "session"]
|
||||
for command in commands:
|
||||
with self.subTest(command=command):
|
||||
completed = subprocess.run(
|
||||
@@ -162,6 +175,128 @@ class StemctlContractTests(unittest.TestCase):
|
||||
self.assertIn("runtime\tFAIL\tdocker is not supported", rendered)
|
||||
self.assertIn("rootless\tFAIL\trootless Podman is required", rendered)
|
||||
|
||||
def test_manifest_is_authoritative_and_maps_logical_series_directory(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
workspace = root / "workspace"
|
||||
info = workspace / "meta" / "workspace-info"
|
||||
(info / "series").mkdir(parents=True)
|
||||
physical_series = workspace / "series" / "freertos"
|
||||
(physical_series / "card-one").mkdir(parents=True)
|
||||
(physical_series / "unregistered-extra").mkdir()
|
||||
(workspace / "series" / ".hidden").mkdir()
|
||||
source_series = root / "original" / "series" / "freertos"
|
||||
(source_series / "card-one").mkdir(parents=True)
|
||||
(info / "series" / "freertos-c.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "freertos-c",
|
||||
"source_org": "edu-freertos-c",
|
||||
"workspace_dir": "freertos",
|
||||
"source_dir": "freertos",
|
||||
"cards": [{"id": "FC01", "repo": "card-one", "title": "First card"}],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(info / "workspace.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"organizations": [
|
||||
{"id": "edu", "role": "control-plane"},
|
||||
{"id": "edu-freertos-c", "role": "series", "series": ["freertos-c"]},
|
||||
],
|
||||
"series": [{"id": "freertos-c", "file": "series/freertos-c.json"}],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config_path = root / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"workspace_root": str(workspace),
|
||||
"original_root": str(root / "original"),
|
||||
"original_series_root": str(root / "original" / "series"),
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = rvctl.load_config(config_path)
|
||||
|
||||
series_output = io.StringIO()
|
||||
cards_output = io.StringIO()
|
||||
with redirect_stdout(series_output):
|
||||
rvctl.print_series(config)
|
||||
with redirect_stdout(cards_output):
|
||||
rvctl.print_cards(config, "freertos-c")
|
||||
|
||||
self.assertIn("freertos-c\t1\t1", series_output.getvalue())
|
||||
self.assertNotIn(".hidden", series_output.getvalue())
|
||||
self.assertIn("FC01\tcard-one\tworkspace\tFirst card", cards_output.getvalue())
|
||||
self.assertNotIn("unregistered-extra", cards_output.getvalue())
|
||||
self.assertEqual(rvctl.workspace_series_path(config, "freertos-c"), physical_series)
|
||||
self.assertEqual(rvctl.source_series_path(config, "freertos-c"), source_series)
|
||||
self.assertEqual(rvctl.resolve_card_info(config, "freertos-c", "FC01").repo, "card-one")
|
||||
|
||||
def test_workspace_catalog_audit_accepts_registered_series_org(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
workspace = root / "workspace"
|
||||
info = workspace / "meta" / "workspace-info"
|
||||
info.mkdir(parents=True)
|
||||
(info / "workspace.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"organizations": [
|
||||
{"id": "edu", "role": "control-plane"},
|
||||
{"id": "edu-freertos-c", "role": "series", "series": ["freertos-c"]},
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"id": "freertos-c",
|
||||
"source_org": "edu-freertos-c",
|
||||
"cards": [{"id": "FC01", "repo": "card-one"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config_path = root / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps({"workspace_root": str(workspace), "original_root": str(root / "original")}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = rvctl.load_config(config_path)
|
||||
checks = rvctl.audit_workspace_catalog(config)
|
||||
self.assertFalse(any(status == "FAIL" for _, status, _ in checks), checks)
|
||||
self.assertIn(("series:freertos-c", "ok", "source_org=edu-freertos-c"), checks)
|
||||
|
||||
def test_workspace_catalog_audit_rejects_implicit_source_org(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
workspace = root / "workspace"
|
||||
info = workspace / "meta" / "workspace-info"
|
||||
info.mkdir(parents=True)
|
||||
(info / "workspace.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"organizations": [{"id": "edu", "role": "control-plane"}],
|
||||
"series": [{"id": "freertos-c", "cards": [{"id": "FC01", "repo": "card-one"}]}],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config_path = root / "config.json"
|
||||
config_path.write_text(
|
||||
json.dumps({"workspace_root": str(workspace), "original_root": str(root / "original")}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = rvctl.load_config(config_path)
|
||||
checks = rvctl.audit_workspace_catalog(config)
|
||||
self.assertIn(("series:freertos-c", "FAIL", "missing explicit source_org"), checks)
|
||||
|
||||
def test_existing_legacy_workspace_is_detected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
@@ -277,6 +412,372 @@ class StemctlContractTests(unittest.TestCase):
|
||||
self.assertEqual(args.mcp_kind, "nvim")
|
||||
self.assertEqual(args.instance, "hazard3-inf-bss-task1")
|
||||
|
||||
def test_mcp_select_accepts_instance_or_container_id(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
by_instance = parser.parse_args(["mcp", "select", "rp2350-pointers-final"])
|
||||
by_container = parser.parse_args(["mcp", "select", "3aca2c1c4c7a"])
|
||||
self.assertEqual(by_instance.mcp_kind, "select")
|
||||
self.assertEqual(by_instance.selector, "rp2350-pointers-final")
|
||||
self.assertEqual(by_container.selector, "3aca2c1c4c7a")
|
||||
|
||||
def test_env_sources_selects_a_card_and_supports_read_only_check(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
args = parser.parse_args(["env", "sources", "inf", "pointers", "--check"])
|
||||
self.assertEqual(args.env_command, "sources")
|
||||
self.assertEqual(args.series, "inf")
|
||||
self.assertEqual(args.card, "pointers")
|
||||
self.assertTrue(args.check)
|
||||
|
||||
def test_mcp_select_switches_both_sockets_with_one_current_symlink(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
socket_dir = root / "source"
|
||||
socket_dir.mkdir()
|
||||
nvim_server = socket.socket(socket.AF_UNIX)
|
||||
tmux_server = socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
nvim_server.bind(str(socket_dir / "n.sock"))
|
||||
tmux_server.bind(str(socket_dir / "t.sock"))
|
||||
config = SimpleNamespace(socket_root=root)
|
||||
record = {
|
||||
"runtime": "podman",
|
||||
"container_name": "stem-rp2350-example",
|
||||
"container_id": "3aca2c1c4c7a" + "0" * 52,
|
||||
"profile": "rp2350",
|
||||
"target": "rp2350-rv",
|
||||
"instance": "rp2350-example",
|
||||
"instance_key": "example000001",
|
||||
"thread_key": "thread000001",
|
||||
"socket_dir": str(socket_dir),
|
||||
}
|
||||
with mock.patch.object(rvctl, "resolve_mcp_record", return_value=record), mock.patch.object(
|
||||
rvctl,
|
||||
"validated_mcp_record",
|
||||
return_value=(record, socket_dir / "n.sock", socket_dir / "t.sock"),
|
||||
), redirect_stdout(io.StringIO()):
|
||||
rvctl.select_mcp_instance(config, "rp2350-example")
|
||||
current = root / "mcp-selected" / "current"
|
||||
self.assertEqual(os.readlink(current), "targets/3aca2c1c4c7a")
|
||||
self.assertEqual((current / "n.sock").resolve(), socket_dir / "n.sock")
|
||||
self.assertEqual((current / "t.sock").resolve(), socket_dir / "t.sock")
|
||||
finally:
|
||||
nvim_server.close()
|
||||
tmux_server.close()
|
||||
|
||||
def test_session_parser_exposes_numbered_target_and_stage_controls(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"session",
|
||||
"reset",
|
||||
"hazard3-sim",
|
||||
"--series",
|
||||
"1",
|
||||
"--card",
|
||||
"7",
|
||||
"--task",
|
||||
"4",
|
||||
"--phase",
|
||||
"2",
|
||||
"--step",
|
||||
"3",
|
||||
"--pane",
|
||||
"current:0",
|
||||
]
|
||||
)
|
||||
self.assertEqual(args.session_command, "reset")
|
||||
self.assertEqual(args.card, "7")
|
||||
self.assertEqual(args.task, "4")
|
||||
self.assertEqual(args.phase, "2")
|
||||
self.assertEqual(args.step, "3")
|
||||
self.assertEqual(args.pane, "current:0")
|
||||
|
||||
def test_session_phase_local_step_falls_back_after_global_number(self) -> None:
|
||||
entries = []
|
||||
for phase_index, numbers in enumerate(((1, 2), (6, 7, 8))):
|
||||
for step_index, number in enumerate(numbers):
|
||||
entries.append(
|
||||
{
|
||||
"task": {"id": "task04", "label": "Task04", "index": 0},
|
||||
"block": {"id": "flow", "label": "Flow", "index": 0},
|
||||
"phase": {
|
||||
"id": f"phase-{phase_index + 1}",
|
||||
"label": f"Phase {phase_index + 1}",
|
||||
"index": phase_index,
|
||||
},
|
||||
"step": {
|
||||
"id": f"step-{number}",
|
||||
"label": f"Step {number}",
|
||||
"number": number,
|
||||
"index": step_index,
|
||||
"global_index": len(entries),
|
||||
},
|
||||
"snapshot": {"ref": f"task04.step-{number}", "index": step_index},
|
||||
}
|
||||
)
|
||||
args = SimpleNamespace(
|
||||
block=None,
|
||||
phase="2",
|
||||
step="3",
|
||||
snapshot=None,
|
||||
stage=None,
|
||||
)
|
||||
selected = rvctl.resolve_navigation_entry({"entries": entries}, "task04_address_arithmetic_alloc", args)
|
||||
self.assertEqual(selected["phase"]["id"], "phase-2")
|
||||
self.assertEqual(selected["step"]["number"], 8)
|
||||
|
||||
def test_session_material_status_has_three_polish_values(self) -> None:
|
||||
entry = {
|
||||
"step": {"id": "one"},
|
||||
"snapshot": {"ref": "task04.one"},
|
||||
}
|
||||
self.assertEqual(
|
||||
rvctl.navigation_entry_status(
|
||||
entry,
|
||||
{
|
||||
"task04.one": {
|
||||
"stop": {"symbol": "main", "offset": 0},
|
||||
"verify": {"expressions": []},
|
||||
}
|
||||
},
|
||||
),
|
||||
"opracowane",
|
||||
)
|
||||
self.assertEqual(rvctl.navigation_entry_status(entry, {}), "robocze")
|
||||
self.assertEqual(rvctl.development_status([]), "brak")
|
||||
|
||||
def test_session_card_number_uses_manifest_order(self) -> None:
|
||||
cards = [
|
||||
rvctl.CardInfo("inf", "first", "first", "main", "", "a", "b", "r1", "r1a", "main"),
|
||||
rvctl.CardInfo("inf", "second", "second", "main", "", "a", "b", "r1", "r1a", "main"),
|
||||
]
|
||||
config = SimpleNamespace(defaults={})
|
||||
with mock.patch.object(rvctl, "source_card_infos", return_value=cards):
|
||||
self.assertEqual(rvctl.resolve_session_card(config, "inf", "2"), "second")
|
||||
|
||||
def test_session_rejects_destructive_online_reset_without_pane_in_preflight(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"session",
|
||||
"reset",
|
||||
"hazard3-sim",
|
||||
"--series",
|
||||
"inf",
|
||||
"--card",
|
||||
"7",
|
||||
"--task",
|
||||
"4",
|
||||
"--pane",
|
||||
"none",
|
||||
"--step",
|
||||
"12",
|
||||
]
|
||||
)
|
||||
with self.assertRaisesRegex(SystemExit, "requires a debugger pane"):
|
||||
rvctl.validate_session_command_args(args)
|
||||
|
||||
def test_session_rejects_profile_target_pair_before_mutation(self) -> None:
|
||||
args = SimpleNamespace(
|
||||
series="inf",
|
||||
card="7",
|
||||
task="4",
|
||||
profile="hazard3-sim",
|
||||
target="rp2350-rv",
|
||||
instance=None,
|
||||
)
|
||||
with (
|
||||
mock.patch.object(rvctl, "resolve_session_series", return_value="inf"),
|
||||
mock.patch.object(rvctl, "resolve_session_card", return_value="pointers"),
|
||||
mock.patch.object(rvctl, "resolve_workspace_card_path", return_value=Path("/tmp/card")),
|
||||
mock.patch.object(rvctl, "resolve_submission_task", return_value="task04"),
|
||||
self.assertRaisesRegex(SystemExit, "does not belong to profile hazard3-sim"),
|
||||
):
|
||||
rvctl.resolve_session_target(SimpleNamespace(), args)
|
||||
|
||||
def test_session_validates_pane_before_container_commands(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
args = parser.parse_args(
|
||||
["session", "reset", "hazard3-sim", "--series", "inf", "--card", "7", "--task", "4"]
|
||||
)
|
||||
target = rvctl.SessionTarget(
|
||||
"inf", "pointers", Path("/tmp/card"), "task04_example", "hazard3-sim",
|
||||
"hazard3-baremetal", "hazard3-sim-inf-pointers-t4"
|
||||
)
|
||||
with (
|
||||
mock.patch.object(rvctl, "resolve_session_target", return_value=target),
|
||||
mock.patch.object(rvctl, "resolve_tmux_session_pane", side_effect=SystemExit("protected")),
|
||||
mock.patch.object(subprocess, "run") as run,
|
||||
self.assertRaisesRegex(SystemExit, "protected"),
|
||||
):
|
||||
rvctl.run_session(SimpleNamespace(), args)
|
||||
run.assert_not_called()
|
||||
|
||||
def test_session_start_pane_none_applies_offline_stage(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
args = parser.parse_args(
|
||||
[
|
||||
"session", "start", "hazard3-sim", "--series", "inf", "--card", "7",
|
||||
"--task", "4", "--pane", "none", "--step", "12", "--offline"
|
||||
]
|
||||
)
|
||||
target = rvctl.SessionTarget(
|
||||
"inf", "pointers", Path("/tmp/card"), "task04_example", "hazard3-sim",
|
||||
"hazard3-baremetal", "hazard3-sim-inf-pointers-t4"
|
||||
)
|
||||
prepared = {
|
||||
"task": {"id": "task04"},
|
||||
"block": {"id": "flow"},
|
||||
"phase": {"id": "alloc-5"},
|
||||
"step": {"id": "commit", "number": 12},
|
||||
"snapshot": {"ref": "task04.alloc5.commit"},
|
||||
}
|
||||
with (
|
||||
mock.patch.object(rvctl, "resolve_session_target", return_value=target),
|
||||
mock.patch.object(rvctl, "prepare_session_stage", return_value=prepared),
|
||||
mock.patch.object(rvctl, "session_env_command", return_value=("true", {"command": "true"})),
|
||||
mock.patch.object(rvctl, "select_session_stage") as select_stage,
|
||||
mock.patch.object(subprocess, "run"),
|
||||
redirect_stdout(io.StringIO()),
|
||||
):
|
||||
rvctl.run_session(SimpleNamespace(), args)
|
||||
select_stage.assert_called_once_with(
|
||||
mock.ANY, target, args, online=False, prepared_entry=prepared
|
||||
)
|
||||
|
||||
def test_session_stage_requires_explicit_selector_and_finite_timeout(self) -> None:
|
||||
parser = rvctl.build_parser()
|
||||
args = parser.parse_args(
|
||||
["session", "stage", "hazard3-sim", "--series", "inf", "--card", "7", "--task", "4"]
|
||||
)
|
||||
with self.assertRaisesRegex(SystemExit, "requires a UML selector"):
|
||||
rvctl.validate_session_command_args(args)
|
||||
with redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
|
||||
parser.parse_args(
|
||||
[
|
||||
"session", "stage", "hazard3-sim", "--series", "inf", "--card", "7",
|
||||
"--task", "4", "--first", "--timeout", "nan"
|
||||
]
|
||||
)
|
||||
|
||||
def test_session_protects_codex_owned_pane_by_pid_marker(self) -> None:
|
||||
completed = subprocess.CompletedProcess(
|
||||
[], 0, stdout="1:0.0\t%3\t/tmp\t999\t999\tthread-1\n", stderr=""
|
||||
)
|
||||
with (
|
||||
mock.patch.object(rvctl, "mark_invoking_codex_pane"),
|
||||
mock.patch.object(subprocess, "run", return_value=completed),
|
||||
mock.patch.dict(os.environ, {"TMUX_PANE": "%33"}),
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "owned by Codex"):
|
||||
rvctl.resolve_tmux_session_pane("%3")
|
||||
self.assertEqual(rvctl.resolve_tmux_session_pane("%3", force=True)[1], "%3")
|
||||
|
||||
def test_session_identity_is_fail_closed_on_uuid_or_hash_mismatch(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
card_path = Path(temporary)
|
||||
(card_path / "json").mkdir()
|
||||
source = {
|
||||
"card": {
|
||||
"id": "card-1",
|
||||
"uuid": "uuid-1",
|
||||
"version": "v1",
|
||||
}
|
||||
}
|
||||
(card_path / "json/card_source.json").write_text(json.dumps(source), encoding="utf-8")
|
||||
target = rvctl.SessionTarget(
|
||||
"inf", "card", card_path, "task04", "hazard3-sim",
|
||||
"hazard3-baremetal", "instance"
|
||||
)
|
||||
served = {
|
||||
"id": "card-1",
|
||||
"uuid": "different",
|
||||
"version": "v1",
|
||||
"source_sha256": "different",
|
||||
}
|
||||
with mock.patch.object(rvctl, "card_api_json", return_value=served):
|
||||
with self.assertRaisesRegex(SystemExit, "uuid, source_sha256"):
|
||||
rvctl.verify_card_api_identity("http://127.0.0.1:8080", target, 1)
|
||||
|
||||
def test_session_cancelled_checkpoint_is_not_published(self) -> None:
|
||||
target = rvctl.SessionTarget(
|
||||
"inf", "pointers", Path("/tmp/card"), "task04", "hazard3-sim",
|
||||
"hazard3-baremetal", "hazard3-sim-inf-pointers-t4"
|
||||
)
|
||||
args = SimpleNamespace(
|
||||
card_url="http://127.0.0.1:8080",
|
||||
timeout=1,
|
||||
focus="step",
|
||||
control=False,
|
||||
)
|
||||
entry = {
|
||||
"task": {"id": "task04"},
|
||||
"block": {"id": "allocator-flow"},
|
||||
"phase": {"id": "alloc-5"},
|
||||
"step": {"id": "alloc5-commit", "number": 12},
|
||||
"snapshot": {"ref": "task04.alloc5.commit"},
|
||||
}
|
||||
record = {
|
||||
"container_id": "abc123",
|
||||
"instance": target.instance,
|
||||
"profile": target.profile,
|
||||
"target": target.target,
|
||||
}
|
||||
api = mock.Mock(return_value={"status": "cancelled", "message": "newer request"})
|
||||
with (
|
||||
mock.patch.object(rvctl, "resolve_mcp_record", return_value=record),
|
||||
mock.patch.object(rvctl, "validated_mcp_record"),
|
||||
mock.patch.object(rvctl, "validate_mcp_card_mount"),
|
||||
mock.patch.object(rvctl, "select_mcp_instance"),
|
||||
mock.patch.object(
|
||||
rvctl,
|
||||
"verify_card_api_identity",
|
||||
return_value={
|
||||
"id": "card-1",
|
||||
"uuid": "uuid-1",
|
||||
"version": "v1",
|
||||
"source_sha256": "abc123",
|
||||
},
|
||||
),
|
||||
mock.patch.object(rvctl, "card_api_json", api),
|
||||
self.assertRaisesRegex(SystemExit, "did not reach ready: cancelled"),
|
||||
):
|
||||
rvctl.select_session_stage(SimpleNamespace(), target, args, True, entry)
|
||||
self.assertEqual(api.call_count, 1)
|
||||
payload = api.call_args.kwargs["payload"]
|
||||
self.assertEqual(payload["expected_identity"]["source_sha256"], "abc123")
|
||||
|
||||
def test_mcp_registry_profile_and_target_must_match_container_labels(self) -> None:
|
||||
record = {
|
||||
"runtime": "podman",
|
||||
"container_name": "container",
|
||||
"container_id": "abc123",
|
||||
"instance_key": "instance-key",
|
||||
"profile": "hazard3-sim",
|
||||
"target": "hazard3-baremetal",
|
||||
"socket_dir": "/tmp/not-used",
|
||||
}
|
||||
inspection = [{
|
||||
"Id": "abc123",
|
||||
"Config": {"Labels": {
|
||||
"edu.stem.instance-key": "instance-key",
|
||||
"edu.stem.profile": "hazard3-sim",
|
||||
"edu.stem.target": "rp2350-rv",
|
||||
}},
|
||||
}]
|
||||
completed = subprocess.CompletedProcess([], 0, stdout=json.dumps(inspection), stderr="")
|
||||
with (
|
||||
mock.patch.object(rvctl.shutil, "which", return_value="/usr/bin/podman"),
|
||||
mock.patch.object(subprocess, "run", return_value=completed),
|
||||
self.assertRaisesRegex(SystemExit, "Stale MCP registry"),
|
||||
):
|
||||
rvctl.validated_mcp_record(record)
|
||||
|
||||
def test_session_task_matching_is_anchored(self) -> None:
|
||||
self.assertTrue(rvctl.same_task("task04", "task04_address_arithmetic_alloc"))
|
||||
self.assertFalse(rvctl.same_task("notask04", "task04_address_arithmetic_alloc"))
|
||||
self.assertFalse(rvctl.same_task("figure2026", "task04_address_arithmetic_alloc"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
"original_series_root": "~/dev/edu/repos/rv/series",
|
||||
"workspace_root": "~/dev/workspace/stem",
|
||||
"legacy_workspace_root": "~/dev/workspace/rv",
|
||||
"workspace_info_url": "http://77.90.8.171:3001/edu-workspace/workspace-info.git",
|
||||
"workspace_info_url": "https://zsl-gitea.mpabi.pl/edu/workspace-info.git",
|
||||
"env_tool_url": "http://77.90.8.171:3001/edu-tools/rv32i-hazard3-student-env.git",
|
||||
"tools_root_candidates": [
|
||||
"~/dev/workspace/stem/tools/rv32i-hazard3-student-env",
|
||||
@@ -14,17 +14,17 @@
|
||||
"~/dev/edu/repos/rv/rv32i-hazard3-env"
|
||||
],
|
||||
"git": {
|
||||
"base_url": "http://77.90.8.171:3001",
|
||||
"source_org": "edu-inf",
|
||||
"base_url": "https://zsl-gitea.mpabi.pl",
|
||||
"source_org": "edu",
|
||||
"answer_org": "c2025-1a-inf",
|
||||
"source_remote": "r1",
|
||||
"answer_remote": "r1a",
|
||||
"origin_remote": "origin",
|
||||
"fallback_branch": "build"
|
||||
"fallback_branch": "main"
|
||||
},
|
||||
"defaults": {
|
||||
"series": "inf",
|
||||
"card": "bss",
|
||||
"series": "freertos-c",
|
||||
"card": "FC02",
|
||||
"task": "task1",
|
||||
"editor": "nvim",
|
||||
"instance": "shell",
|
||||
|
||||
Reference in New Issue
Block a user