Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0548f0e56 | |||
| 4dda6de387 | |||
| 8c7a67c471 | |||
| 319e8b84f4 | |||
| 8d1379ec66 | |||
| 45257057df | |||
| aa5335aca0 | |||
| 1d8f16303e | |||
| 1e1c124c9f | |||
| 1a98c14654 |
@@ -1,8 +1,9 @@
|
|||||||
# STEM Launcher
|
# STEM Launcher
|
||||||
|
|
||||||
`stemctl` jest hostowym wejściem do kart pracy, Git i trzech źródłowo
|
`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ć
|
budowanych kontenerów STEM. Zdalne repo nosi kanoniczną nazwę
|
||||||
`rv-launcher`; `rvctl` pozostaje ostrzegającym wrapperem zgodności.
|
`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
|
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
|
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,
|
artefakty są bind-mountem na hoście. Kontenery nie dostają kluczy SSH, tokenów,
|
||||||
socketu Podmana/Dockera ani całego `/dev`.
|
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
|
## Szybki start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -41,6 +59,23 @@ socketu Podmana/Dockera ani całego `/dev`.
|
|||||||
Pierwsze wywołanie danego profilu może potrwać, ponieważ buduje go ze
|
Pierwsze wywołanie danego profilu może potrwać, ponieważ buduje go ze
|
||||||
źródłowego Dockerfile. Kolejne korzystają z lokalnych warstw cache.
|
ź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:
|
Wymuszenie samego przygotowania środowiska:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -68,6 +103,33 @@ Domyślny układ:
|
|||||||
└── tokens/tokens.json
|
└── 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
|
Jeżeli istnieje tylko starszy `~/dev/workspace/rv`, launcher wykrywa go bez
|
||||||
niszczenia danych. Jawna migracja:
|
niszczenia danych. Jawna migracja:
|
||||||
|
|
||||||
@@ -101,6 +163,30 @@ się w wybranym kontenerze:
|
|||||||
Oddzielne wpisy klienta MCP mogą wskazywać inne nazwy instancji, więc jeden
|
Oddzielne wpisy klienta MCP mogą wskazywać inne nazwy instancji, więc jeden
|
||||||
Codex obsługuje kilka kontenerów bez globalnego „ostatniego socketu”.
|
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
|
## Komputer zdalny
|
||||||
|
|
||||||
Na komputer ucznia wchodzimy wyłącznie SSH z parą kluczy. Launcher, Git,
|
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)
|
- [plan architektury](doc/architecture-plan.md)
|
||||||
- [kontenery i interfejs](doc/containers.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)
|
- [migracja nazw i workspace](doc/migration-stem-launcher.md)
|
||||||
- [serie i karty](doc/series.md)
|
- [serie i karty](doc/series.md)
|
||||||
- [tokeny Gitea](doc/tokens.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.
|
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
|
## Aktualny fundament
|
||||||
|
|
||||||
`rv32i-hazard3-student-env` dostarcza źródła wspólnego modelu:
|
`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
|
`stemctl` traktuje te pliki jako szczegóły implementacyjne. Użytkownik i agent
|
||||||
dostają komendy wyższego poziomu.
|
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
|
## Role profili
|
||||||
|
|
||||||
Profil `hazard3-sim`:
|
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
|
artefaktu. Bez niego odmawia wykonania, chyba że użytkownik jawnie zaakceptuje
|
||||||
istniejący firmware. Zapobiega to testowaniu starego programu.
|
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
|
## Instancje i sockety MCP
|
||||||
|
|
||||||
Stabilna nazwa logiczna:
|
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
|
zweryfikowanego kontenera. Neovim MCP publikuje również stan i komendy
|
||||||
Termdebug/nvim-dap.
|
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
|
MCP nie jest czwartym kontenerem. Bridge jest częścią `dev-ui-base`, a hostowy
|
||||||
resolver jest częścią `stem-launcher`.
|
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`
|
# Migracja `rv-launcher` do `stem-launcher`
|
||||||
|
|
||||||
Status: M1–M3 wdrożone lokalnie; M4 (zmiana nazwy zdalnego repo) celowo
|
Status: M1–M4 wdrożone; M5 pozostaje okresem zgodności
|
||||||
odłożone do zakończenia okresu zgodności
|
|
||||||
Data: 2026-07-14
|
Data: 2026-07-14
|
||||||
|
|
||||||
## Dlaczego zmieniamy nazwę
|
## Dlaczego zmieniamy nazwę
|
||||||
@@ -34,10 +33,10 @@ na serwerze. Odwrotna kolejność zepsułaby bootstrap, token records, remotes,
|
|||||||
### M1 — alias CLI (wdrożone)
|
### M1 — alias CLI (wdrożone)
|
||||||
|
|
||||||
- dodać wykonywalny `stemctl` wskazujący tę samą implementację;
|
- 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`;
|
- pomoc i nowe materiały pokazują wyłącznie `stemctl`;
|
||||||
- `rvctl` drukuje jednorazowe ostrzeżenie o wycofaniu na stderr, ale zachowuje
|
- `rvctl` nie dopisuje ostrzeżeń do stderr i zachowuje format stdout/stderr
|
||||||
format stdout potrzebny skryptom.
|
potrzebny istniejącym skryptom.
|
||||||
|
|
||||||
### M2 — konfiguracja i ścieżki (wdrożone)
|
### 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;
|
- kontenery otrzymują labels z nazwą kanoniczną, nie aliasem;
|
||||||
- wyniki zapisują nazwę kanoniczną oraz opcjonalne `requested_alias`.
|
- 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`;
|
1. zmianę nazwy na `edu-tools/stem-launcher` przez API Gitea;
|
||||||
2. zaktualizować `workspace-info`, bootstrap i token records;
|
2. aktualizację lokalnego `origin`, bootstrapu, dokumentacji i przykładów
|
||||||
3. sprawdzić clone/fetch/push przez nowe URL;
|
token records;
|
||||||
4. pozostawić pod `edu-tools/rv-launcher` przekierowanie albo małe repo z
|
3. weryfikację clone/fetch/push przez nowy URL;
|
||||||
komunikatem migracyjnym, zależnie od możliwości Gitea;
|
4. weryfikację przekierowania starej nazwy przez Gitea;
|
||||||
5. nie usuwać starej nazwy podczas trwającego semestru.
|
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
|
Operację administracyjną wykonano po SSH do VPS i przez ograniczony token API;
|
||||||
samą aktualizację dokumentacji.
|
sekret nie znajduje się w repo ani w konfiguracji launchera.
|
||||||
|
|
||||||
### M5 — wycofanie kompatybilności
|
### M5 — wycofanie kompatybilności
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -19,11 +19,11 @@ Workspace służy do klonów roboczych i ćwiczeń:
|
|||||||
Typowy układ:
|
Typowy układ:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/edu/repos/rv/rv-launcher
|
~/dev/edu/repos/stem/stem-launcher
|
||||||
~/dev/edu/repos/rv/rv32i-hazard3-env
|
~/dev/edu/repos/rv/rv32i-hazard3-env
|
||||||
~/dev/edu/repos/rv/series/<seria>/<karta>
|
~/dev/edu/repos/rv/series/<seria>/<karta>
|
||||||
~/dev/workspace/rv/meta/workspace-info
|
~/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/tools/rv32i-hazard3-env
|
||||||
~/dev/workspace/rv/series/<seria>/<karta>
|
~/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:
|
Repo treningowe launchera trzymaj pod:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/dev/workspace/rv/tools/rv-launcher
|
~/dev/workspace/stem/tools/stem-launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
Podstawowy bootstrap wygląda tak:
|
Podstawowy bootstrap wygląda tak:
|
||||||
@@ -54,8 +54,8 @@ Podstawowy bootstrap wygląda tak:
|
|||||||
```bash
|
```bash
|
||||||
mkdir -p ~/dev/workspace/rv/tools
|
mkdir -p ~/dev/workspace/rv/tools
|
||||||
cd ~/dev/workspace/rv/tools
|
cd ~/dev/workspace/rv/tools
|
||||||
git clone http://77.90.8.171:3001/edu-tools/rv-launcher.git
|
git clone http://77.90.8.171:3001/edu-tools/stem-launcher.git
|
||||||
cd rv-launcher
|
cd stem-launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
Główne pliki CLI:
|
Główne pliki CLI:
|
||||||
@@ -89,7 +89,7 @@ tokenem do zdalnego endpointu. Jeśli launcher zobaczy URL w formacie
|
|||||||
Przykład:
|
Przykład:
|
||||||
|
|
||||||
```bash
|
```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 fetch r1 main
|
||||||
git switch --track -c main r1/main
|
git switch --track -c main r1/main
|
||||||
```
|
```
|
||||||
@@ -120,7 +120,7 @@ Minimalny format pliku:
|
|||||||
},
|
},
|
||||||
"user": "u1",
|
"user": "u1",
|
||||||
"org": "edu-tools",
|
"org": "edu-tools",
|
||||||
"repo": "rv-launcher"
|
"repo": "stem-launcher"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -757,7 +757,7 @@ Typowy wynik:
|
|||||||
remotes
|
remotes
|
||||||
item remote kind server proto host org repo user token result url
|
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:
|
Przykład:
|
||||||
@@ -784,7 +784,7 @@ Typowy wynik:
|
|||||||
tokens
|
tokens
|
||||||
item server proto host org repo user remote token_ref token valid scope org repo
|
item server proto host org repo user remote token_ref token valid scope org repo
|
||||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- 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
|
`token_ref` jest komórką stałej szerokości: nazwa tokena jest po lewej, a marker
|
||||||
@@ -827,8 +827,8 @@ Typowy wynik:
|
|||||||
tokens
|
tokens
|
||||||
item source kind server proto host org repo user remote token valid scope org repo
|
item source kind server proto host org repo user remote token valid scope org repo
|
||||||
---- ------ ----- ------ ----- ------------------ --------- ----------- ---- ------ ------------ ------------------- aAimnopru oawrc- oawr--
|
---- ------ ----- ------ ----- ------------------ --------- ----------- ---- ------ ------------ ------------------- aAimnopru oawrc- oawr--
|
||||||
1 store auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 e59cc...13be forever --------- +++++ ++++
|
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 rv-launcher u1 r1 e59cc...13be
|
2 remote auth gitea http 77.90.8.171:3001 edu-tools stem-launcher u1 r1 e59cc...13be
|
||||||
```
|
```
|
||||||
|
|
||||||
Przykłady:
|
Przykłady:
|
||||||
@@ -866,7 +866,7 @@ Przykłady:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens add r1
|
./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`
|
## `tokens sync remote REMOTE_ID`
|
||||||
@@ -885,7 +885,7 @@ Przykład:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens sync remote r1
|
./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`
|
## `tokens sync store REMOTE_ID`
|
||||||
@@ -996,7 +996,7 @@ token_path<TAB>...
|
|||||||
tokens
|
tokens
|
||||||
item server proto host org repo user remote token_ref token valid scope org repo
|
item server proto host org repo user remote token_ref token valid scope org repo
|
||||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- 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
|
status
|
||||||
item<TAB>value
|
item<TAB>value
|
||||||
|
|||||||
+27
-20
@@ -11,24 +11,30 @@ Komendy kart pracy dzielimy na trzy poziomy:
|
|||||||
- `series cards` - operacje na kartach w ramach serii
|
- `series cards` - operacje na kartach w ramach serii
|
||||||
- `series cards tasks` - operacje na zadaniach w ramach karty
|
- `series cards tasks` - operacje na zadaniach w ramach karty
|
||||||
|
|
||||||
`rvctl` tworzy `series_root`, jeżeli katalog jeszcze nie istnieje. Lista serii
|
`stemctl` tworzy `series_root`, jeżeli katalog jeszcze nie istnieje. Gdy
|
||||||
i kart pochodzi docelowo z publicznego manifestu
|
`workspace-info` jest dostępny, manifest jest jedynym źródłem listy serii i
|
||||||
`~/dev/workspace/rv/meta/workspace-info/workspace.json`. Jeżeli manifestu nie
|
kart. Przypadkowe katalogi robocze nie pojawiają się w katalogu. Fallback przez
|
||||||
ma, komendy zależne od serii automatycznie klonują repo
|
`original_series_root` działa wyłącznie dla starego workspace bez manifestu.
|
||||||
`edu-workspace/workspace-info`. Jeżeli klonowanie nie jest możliwe, narzędzie
|
|
||||||
używa starego fallbacku przez `original_series_root`.
|
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
|
## Szybki przepływ
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens compare
|
./stemctl workspace audit
|
||||||
./rvctl series list
|
./stemctl tokens compare
|
||||||
./rvctl series use inf
|
./stemctl series list
|
||||||
./rvctl series cards list inf
|
./stemctl series use freertos-c
|
||||||
./rvctl card use bss
|
./stemctl series cards list freertos-c
|
||||||
./rvctl series cards fetch inf bss
|
./stemctl card use FC02
|
||||||
./rvctl tasks list
|
./stemctl series cards fetch freertos-c FC02
|
||||||
./rvctl tasks switch 1
|
./stemctl tasks list
|
||||||
|
./stemctl tasks switch 1
|
||||||
```
|
```
|
||||||
|
|
||||||
Dla karty `bss` właściwym repo jest `lab-rv32i-strlen-bss-data-stack`.
|
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`
|
### `series list`
|
||||||
|
|
||||||
Listuje dostępne serie z manifestu `workspace-info` oraz liczbę kart już
|
Listuje wyłącznie operacyjne serie z manifestu `workspace-info` oraz liczbę
|
||||||
pobranych do lokalnego workspace. Jeżeli manifestu jeszcze nie ma, komenda
|
kart już obecnych w lokalnym workspace. Logiczna seria może wskazywać wspólny,
|
||||||
pobiera go automatycznie. Jeżeli `~/dev/workspace/rv/series` nie istnieje,
|
przejściowy katalog fizyczny przez `workspace_dir`; na przykład `freertos-c`
|
||||||
komenda tworzy ten katalog.
|
jest obecnie mapowane na `series/freertos`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl series list
|
./rvctl series list
|
||||||
@@ -49,8 +55,9 @@ komenda tworzy ten katalog.
|
|||||||
Typowy wynik:
|
Typowy wynik:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
fiz 3 0
|
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
|
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
|
`org/repo`. Git remote służy tylko do operacji Git (`fetch`, `push`) albo do
|
||||||
pierwszego wczytania tokena do store.
|
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`.
|
innych repo podaj `--repo PATH`.
|
||||||
|
|
||||||
## Szybki przepływ
|
## Szybki przepływ
|
||||||
@@ -17,7 +17,7 @@ innych repo podaj `--repo PATH`.
|
|||||||
Startujemy od git remota z tokenem w URL-u:
|
Startujemy od git remota z tokenem w URL-u:
|
||||||
|
|
||||||
```bash
|
```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:
|
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:
|
`org` i `repo` w `tokens.json`, na przykład:
|
||||||
|
|
||||||
```text
|
```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`
|
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:
|
Opcjonalnie można podać URL ręcznie:
|
||||||
|
|
||||||
```bash
|
```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:
|
Jeśli remote ma już inne credentials, użyj:
|
||||||
@@ -265,7 +265,7 @@ domyślnie pochodzi z `workspace.json`.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens add r1
|
./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
|
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
|
```bash
|
||||||
./rvctl tokens stats
|
./rvctl tokens stats
|
||||||
./rvctl tokens stats --repo ~/dev/workspace/rv/tools/rv-launcher
|
./rvctl tokens stats --repo ~/dev/workspace/stem/tools/stem-launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uprawnienia
|
## Uprawnienia
|
||||||
@@ -384,7 +384,7 @@ Minimalny przykład:
|
|||||||
},
|
},
|
||||||
"user": "u1",
|
"user": "u1",
|
||||||
"org": "edu-tools",
|
"org": "edu-tools",
|
||||||
"repo": "rv-launcher"
|
"repo": "stem-launcher"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://edu-tools.local/rv-launcher/tokens.schema.json",
|
"$id": "https://edu-tools.local/stem-launcher/tokens.schema.json",
|
||||||
"title": "RV launcher tokens",
|
"title": "STEM launcher tokens",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["version", "tokens"],
|
"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
|
lokalne ścieżki workspace, series, socketów, tokenów i adres publicznego
|
||||||
manifestu.
|
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
|
```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:
|
Lokalna kopia manifestu znajduje się w:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/workspace/rv/meta/workspace-info
|
~/dev/workspace/stem/meta/workspace-info
|
||||||
```
|
```
|
||||||
|
|
||||||
Typowy układ:
|
Typowy układ:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
~/dev/workspace/rv
|
~/dev/workspace/stem
|
||||||
├── meta
|
├── meta
|
||||||
│ └── workspace-info
|
│ └── workspace-info
|
||||||
├── tools
|
├── tools
|
||||||
│ └── rv-launcher
|
│ └── stem-launcher
|
||||||
├── tokens
|
├── tokens
|
||||||
│ └── tokens.json
|
│ └── tokens.json
|
||||||
└── series
|
└── series
|
||||||
```
|
```
|
||||||
|
|
||||||
`workspace-info` opisuje serie, karty, repo źródłowe, repo odpowiedzi i branche.
|
`workspace-info` opisuje rejestr organizacji, operacyjne serie, karty, repo
|
||||||
Nie przechowuje tokenów ani lokalnych plików roboczych ucznia.
|
ź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:
|
Aktualizacja manifestu:
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
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
|
export STEM_CLI_NAME=rvctl
|
||||||
exec python3 "$SCRIPT_DIR/rvctl.py" "$@"
|
exec python3 "$SCRIPT_DIR/rvctl.py" "$@"
|
||||||
|
|||||||
+503
-2
@@ -3,10 +3,11 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stderr, redirect_stdout
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@@ -18,9 +19,21 @@ import rvctl # noqa: E402
|
|||||||
|
|
||||||
|
|
||||||
class StemctlContractTests(unittest.TestCase):
|
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:
|
def test_new_command_help_never_crashes(self) -> None:
|
||||||
script = Path(__file__).resolve().parents[1] / "rvctl.py"
|
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:
|
for command in commands:
|
||||||
with self.subTest(command=command):
|
with self.subTest(command=command):
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
@@ -162,6 +175,128 @@ class StemctlContractTests(unittest.TestCase):
|
|||||||
self.assertIn("runtime\tFAIL\tdocker is not supported", rendered)
|
self.assertIn("runtime\tFAIL\tdocker is not supported", rendered)
|
||||||
self.assertIn("rootless\tFAIL\trootless Podman is required", 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:
|
def test_existing_legacy_workspace_is_detected(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
root = Path(temporary)
|
root = Path(temporary)
|
||||||
@@ -277,6 +412,372 @@ class StemctlContractTests(unittest.TestCase):
|
|||||||
self.assertEqual(args.mcp_kind, "nvim")
|
self.assertEqual(args.mcp_kind, "nvim")
|
||||||
self.assertEqual(args.instance, "hazard3-inf-bss-task1")
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
+6
-6
@@ -4,7 +4,7 @@
|
|||||||
"original_series_root": "~/dev/edu/repos/rv/series",
|
"original_series_root": "~/dev/edu/repos/rv/series",
|
||||||
"workspace_root": "~/dev/workspace/stem",
|
"workspace_root": "~/dev/workspace/stem",
|
||||||
"legacy_workspace_root": "~/dev/workspace/rv",
|
"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",
|
"env_tool_url": "http://77.90.8.171:3001/edu-tools/rv32i-hazard3-student-env.git",
|
||||||
"tools_root_candidates": [
|
"tools_root_candidates": [
|
||||||
"~/dev/workspace/stem/tools/rv32i-hazard3-student-env",
|
"~/dev/workspace/stem/tools/rv32i-hazard3-student-env",
|
||||||
@@ -14,17 +14,17 @@
|
|||||||
"~/dev/edu/repos/rv/rv32i-hazard3-env"
|
"~/dev/edu/repos/rv/rv32i-hazard3-env"
|
||||||
],
|
],
|
||||||
"git": {
|
"git": {
|
||||||
"base_url": "http://77.90.8.171:3001",
|
"base_url": "https://zsl-gitea.mpabi.pl",
|
||||||
"source_org": "edu-inf",
|
"source_org": "edu",
|
||||||
"answer_org": "c2025-1a-inf",
|
"answer_org": "c2025-1a-inf",
|
||||||
"source_remote": "r1",
|
"source_remote": "r1",
|
||||||
"answer_remote": "r1a",
|
"answer_remote": "r1a",
|
||||||
"origin_remote": "origin",
|
"origin_remote": "origin",
|
||||||
"fallback_branch": "build"
|
"fallback_branch": "main"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"series": "inf",
|
"series": "freertos-c",
|
||||||
"card": "bss",
|
"card": "FC02",
|
||||||
"task": "task1",
|
"task": "task1",
|
||||||
"editor": "nvim",
|
"editor": "nvim",
|
||||||
"instance": "shell",
|
"instance": "shell",
|
||||||
|
|||||||
Reference in New Issue
Block a user