mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 06:28:11 +02:00
Lot 43 : jobs d'arrière-plan du mode Code tenus par la façade
bash_bg et bash_tail des tours de la façade ne passent plus par /tool/exec du Go : src/tools/jobs.rs porte code_jobs.go. Refus avant lancement (commande vide ou dangereuse, optimisation en cours), journal .loki/jobs/<id>.log dans le dossier de la discussion, processus qui survit au tour, shell sans le jeton interne ; relecture bornée (40 lignes par défaut, 200 au plus), état et durée, arrêt relu aussitôt comme le Go. paths::go_mkdir_all reproduit os.MkdirAll, erreur comprise. Les jobs d'une discussion supprimée s'arrêtent avec elle. L'optimiseur du Go compte ceux de la façade (POST /internal/facade/v1/bgjobs) avant de mesurer ; façade injoignable : les siens seulement. Parité : journal de 24 étapes tiré du Go (parity_jobs_test.go → tests/parity_jobs/jobs.json), rejoué par tests/jobs.rs ; un tour de la façade qui lance un job (tests/lot6.rs) ; TestFacadeJobsArrierePlan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
16 files changed
+1132
-4
No files matched your search
@@ -177,10 +177,25 @@ func toolBashTail(args map[string]any) string {
|
||||
return out + "\n" + tailRunes(tail, toolMaxOutput)
|
||||
}
|
||||
|
||||
// bgJobsRunning : jobs d'arrière-plan encore en vie. L'optimiseur refuse de
|
||||
// mesurer à côté d'eux : un build ou un serveur de dev prend le CPU et le
|
||||
// disque que mesurent ses essais.
|
||||
// bgJobsRunning : jobs d'arrière-plan encore en vie, ceux de la façade
|
||||
// compris quand elle tient la conversation (lot 43 : ses tours lancent les
|
||||
// leurs). L'optimiseur refuse de mesurer à côté d'eux : un build ou un
|
||||
// serveur de dev prend le CPU et le disque que mesurent ses essais.
|
||||
func bgJobsRunning() int {
|
||||
n := localBgJobsRunning()
|
||||
if facadeChat() {
|
||||
var out struct {
|
||||
Running int `json:"running"`
|
||||
}
|
||||
if err := facadePerfJSON("/internal/facade/v1/bgjobs", map[string]any{}, &out); err == nil {
|
||||
n += out.Running
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// localBgJobsRunning : les jobs lancés par ce processus.
|
||||
func localBgJobsRunning() int {
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
n := 0
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Lot 43 : les jobs d'arrière-plan des tours de la façade comptent pour
|
||||
// l'optimiseur du Go ; façade en panne, seuls les siens.
|
||||
func TestFacadeJobsArrierePlan(t *testing.T) {
|
||||
testHome(t)
|
||||
jobsMu.Lock()
|
||||
saved := jobs
|
||||
jobs = map[string]*bgJob{"job1": {ID: "job1"}}
|
||||
jobsMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
jobsMu.Lock()
|
||||
jobs = saved
|
||||
jobsMu.Unlock()
|
||||
})
|
||||
f := demarrerFacade(t)
|
||||
f.internalStatus = 200
|
||||
f.internalBody = `{"running":2}`
|
||||
if n := bgJobsRunning(); n != 3 {
|
||||
t.Fatalf("jobs : %d, attendu 3 (1 ici, 2 à la façade)", n)
|
||||
}
|
||||
vu := f.recu()
|
||||
if len(vu) != 1 || !strings.HasPrefix(vu[0], "POST /internal/facade/v1/bgjobs jeton ") {
|
||||
t.Fatalf("appel : %v", vu)
|
||||
}
|
||||
f.internalStatus = 500
|
||||
if n := bgJobsRunning(); n != 1 {
|
||||
t.Fatalf("façade en panne : %d", n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//go:build linux
|
||||
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parity_jobs_test.go — journal de référence des jobs d'arrière-plan du mode
|
||||
// Code (lot 43 de la migration Rust) : bash_bg et bash_tail, de la commande
|
||||
// refusée au job tué. Pid et durées sont masqués ; l'état relu juste après
|
||||
// un arrêt aussi (le Go relit sans attendre la fin). Rangé dans
|
||||
// rust/loki-rs/tests/parity_jobs/jobs.json ; tests/jobs.rs le rejoue contre
|
||||
// le port.
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteJobs
|
||||
|
||||
const parityJobsFile = "../../rust/loki-rs/tests/parity_jobs/jobs.json"
|
||||
|
||||
type parityJobStep struct {
|
||||
Name string `json:"name"`
|
||||
Tool string `json:"tool"`
|
||||
Args map[string]any `json:"args"`
|
||||
Wait string `json:"wait,omitempty"` // job dont attendre la fin avant l'appel
|
||||
After string `json:"after,omitempty"`
|
||||
Result string `json:"result"`
|
||||
Logs []string `json:"logs"` // journaux présents dans .loki/jobs
|
||||
}
|
||||
|
||||
var (
|
||||
parityJobsPid = regexp.MustCompile(`\(pid [0-9]+\)`)
|
||||
parityJobsSince = regexp.MustCompile(`depuis [0-9hms.]+`)
|
||||
parityJobsState = regexp.MustCompile(`— (en cours|terminé \(exit -1\)) —`)
|
||||
)
|
||||
|
||||
func TestPariteJobs(t *testing.T) {
|
||||
root := testHome(t)
|
||||
if r, err := filepath.EvalSymlinks(root); err == nil {
|
||||
root = r
|
||||
}
|
||||
ws := filepath.Join(root, "ws")
|
||||
if err := os.MkdirAll(ws, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setTaskWorkspace(ws)
|
||||
t.Cleanup(func() { setTaskWorkspace("") })
|
||||
jobsMu.Lock()
|
||||
jobs, jobSeq = map[string]*bgJob{}, 0
|
||||
jobsMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
for _, j := range jobs {
|
||||
if j.Cmd.Process != nil {
|
||||
_ = j.Cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wait := func(id string) {
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
jobsMu.Lock()
|
||||
j := jobs[id]
|
||||
jobsMu.Unlock()
|
||||
if j != nil {
|
||||
j.mu.Lock()
|
||||
done := j.done
|
||||
j.mu.Unlock()
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("job %s : pas fini", id)
|
||||
}
|
||||
var steps []parityJobStep
|
||||
run := func(st parityJobStep) {
|
||||
if st.Wait != "" {
|
||||
wait(st.Wait)
|
||||
}
|
||||
args := map[string]any{}
|
||||
b, _ := json.Marshal(st.Args)
|
||||
_ = json.Unmarshal(b, &args)
|
||||
res := execTool(t.Context(), st.Tool, args, toolCallLabel(st.Tool, args), Caps{Agent: true, Code: true})
|
||||
r := parityJobsPid.ReplaceAllString(res.Result, "(pid {{PID}})")
|
||||
r = parityJobsSince.ReplaceAllString(r, "depuis {{D}}")
|
||||
if st.After == "kill" {
|
||||
r = parityJobsState.ReplaceAllString(r, "— {{ETAT}} —")
|
||||
}
|
||||
st.Result = r
|
||||
st.Logs = []string{}
|
||||
entries, _ := os.ReadDir(filepath.Join(ws, ".loki", "jobs"))
|
||||
for _, e := range entries {
|
||||
st.Logs = append(st.Logs, e.Name())
|
||||
}
|
||||
if st.Args == nil {
|
||||
st.Args = map[string]any{}
|
||||
}
|
||||
steps = append(steps, st)
|
||||
}
|
||||
a := func(kv ...any) map[string]any {
|
||||
m := map[string]any{}
|
||||
for i := 0; i+1 < len(kv); i += 2 {
|
||||
m[kv[i].(string)] = kv[i+1]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
run(parityJobStep{Name: "commande vide", Tool: "bash_bg", Args: a("command", " ")})
|
||||
run(parityJobStep{Name: "commande refusée", Tool: "bash_bg", Args: a("command", "rm -rf /")})
|
||||
run(parityJobStep{Name: "lancement", Tool: "bash_bg", Args: a("command", "echo un; echo deux >&2; printf 'trois\\n\\n'")})
|
||||
run(parityJobStep{Name: "sortie", Wait: "job1", Tool: "bash_tail", Args: a("id", "job1")})
|
||||
run(parityJobStep{Name: "deux lignes", Tool: "bash_tail", Args: a("id", "job1", "lines", 2.0)})
|
||||
run(parityJobStep{Name: "lignes nulles", Tool: "bash_tail", Args: a("id", "job1", "lines", 0.0)})
|
||||
run(parityJobStep{Name: "lignes en texte", Tool: "bash_tail", Args: a("id", "job1", "lines", "1")})
|
||||
run(parityJobStep{Name: "lignes tronquées", Tool: "bash_tail", Args: a("id", "job1", "lines", 1.9)})
|
||||
run(parityJobStep{Name: "arrêt d'un job fini", Tool: "bash_tail", Args: a("id", "job1", "kill", true)})
|
||||
run(parityJobStep{Name: "job inconnu", Tool: "bash_tail", Args: a("id", "job9")})
|
||||
run(parityJobStep{Name: "sans id", Tool: "bash_tail", Args: a()})
|
||||
run(parityJobStep{Name: "sortie en erreur", Tool: "bash_bg", Args: a("command", "exit 3")})
|
||||
run(parityJobStep{Name: "rien d'écrit", Wait: "job2", Tool: "bash_tail", Args: a("id", "job2")})
|
||||
run(parityJobStep{Name: "job long", Tool: "bash_bg", Args: a("command", "sleep 30")})
|
||||
run(parityJobStep{Name: "en cours", Tool: "bash_tail", Args: a("id", "job3")})
|
||||
run(parityJobStep{Name: "arrêt", Tool: "bash_tail", Args: a("id", "job3", "kill", true), After: "kill"})
|
||||
run(parityJobStep{Name: "après l'arrêt", Wait: "job3", Tool: "bash_tail", Args: a("id", "job3")})
|
||||
run(parityJobStep{Name: "beaucoup de lignes", Tool: "bash_bg", Args: a("command", "for i in $(seq 1 300); do echo ligne $i; done")})
|
||||
run(parityJobStep{Name: "quarante par défaut", Wait: "job4", Tool: "bash_tail", Args: a("id", "job4")})
|
||||
run(parityJobStep{Name: "deux cents au plus", Tool: "bash_tail", Args: a("id", "job4", "lines", 500.0)})
|
||||
run(parityJobStep{Name: "sortie démesurée", Tool: "bash_bg", Args: a("command", "for i in $(seq 1 9000); do printf 'é'; done; echo")})
|
||||
run(parityJobStep{Name: "fin gardée", Wait: "job5", Tool: "bash_tail", Args: a("id", "job5")})
|
||||
run(parityJobStep{Name: "octets invalides", Tool: "bash_bg", Args: a("command", "printf 'a\\377b\\n'")})
|
||||
run(parityJobStep{Name: "octets relus", Wait: "job6", Tool: "bash_tail", Args: a("id", "job6")})
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(map[string]any{"steps": steps}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := buf.Bytes()
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityJobsFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityJobsFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityJobsFile)
|
||||
if err != nil {
|
||||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Errorf("les jobs d'arrière-plan ne produisent plus le journal rangé dans %s.\n"+
|
||||
"LOKI_PARITY_UPDATE=1 pour le régénérer si le changement est voulu.", parityJobsFile)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user