The indexer reads the forge through the seat git alone: trees, file, contains and open-pulls, an answer's error a failure (milestone 47/G W4.15a)
The forge module's own tools leave the program and its invokes. The seat's Go contract is feat/47-G-seats' (contracts/go/seats/git); this branch builds against it at the join. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ Builds: design 47 G §310, §312, §317, §428
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/trunk"
|
||||
|
||||
@@ -18,37 +19,75 @@ import (
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
)
|
||||
|
||||
// commitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea): with `paths`, by module directory
|
||||
// (the root spelled ""), the paths relative to it, it answers {root, trees, paths} with paths by directory then path, each
|
||||
// path's git object id; a path not at the commit fails the call naming it (the interface agreed in R4e; mesh-sdk
|
||||
// forgemsg.Forge.Paths; novox/hq ADR 0499 clause 3).
|
||||
const commitTreesTool = "gitea.gitea_commit_trees"
|
||||
// The forge is read through the seat git alone (design 47 *The seat git*; milestone 47/G W4.15): each read is the seat's
|
||||
// verb, `seat:git.<verb>`, its input and answer the seat's contract; an answer that carries an error is a failure,
|
||||
// never an empty answer.
|
||||
|
||||
// toolCaller calls a module's tool, `<module>.<tool>`, as the indexer's own account (mesh-sdk process.Conn.Call).
|
||||
// gitVerb is the address of the seat git's verb.
|
||||
func gitVerb(verb string) string { return "seat:" + git.Seat + "." + verb }
|
||||
|
||||
// toolCaller calls a module's tool, `<module>.<tool>`, or a seat's verb, `seat:<seat>.<verb>`, as the indexer's own account
|
||||
// (mesh-sdk process.Conn.Call).
|
||||
type toolCaller func(key string, body any) (json.RawMessage, error)
|
||||
|
||||
// namedVia asks the forge for the objects of the named paths through call. An answer without the object of every path
|
||||
// asked is an error naming it.
|
||||
// askGit asks one verb of the seat git and decodes its answer into out; an answer whose error is set is the failure it
|
||||
// names, with its kind.
|
||||
func askGit(call toolCaller, verb string, in, out any) error {
|
||||
raw, err := call(gitVerb(verb), in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var failed struct {
|
||||
Error string `json:"error"`
|
||||
ErrorKind git.ErrorKind `json:"error-kind"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &failed); err != nil {
|
||||
return fmt.Errorf("the answer of %s is not its seat's answer: %w", gitVerb(verb), err)
|
||||
}
|
||||
if failed.Error != "" {
|
||||
return fmt.Errorf("%s (%s): %s", gitVerb(verb), failed.ErrorKind, failed.Error)
|
||||
}
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return fmt.Errorf("the answer of %s is not its seat's answer: %w", gitVerb(verb), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// treesOf asks the seat git's trees of a commit.
|
||||
func treesOf(call toolCaller, repository string, in git.TreesInput) (git.TreesAnswer, error) {
|
||||
var a git.TreesAnswer
|
||||
in.Repository = trunk.RepositoryName(repository)
|
||||
if !strings.Contains(in.Repository, "/") {
|
||||
return a, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
return a, askGit(call, git.VerbTrees, in, &a)
|
||||
}
|
||||
|
||||
// fileOf asks the seat git's file at a ref: its content, or absent when the forge holds no such file there.
|
||||
func fileOf(call toolCaller, repository, p, ref string) (content []byte, absent bool, err error) {
|
||||
var a git.FileAnswer
|
||||
in := git.FileInput{Repository: trunk.RepositoryName(repository), Path: p, Ref: ref}
|
||||
if !strings.Contains(in.Repository, "/") {
|
||||
return nil, false, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
if err := askGit(call, git.VerbFile, in, &a); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return []byte(a.Content), a.Absent, nil
|
||||
}
|
||||
|
||||
// namedVia asks the forge for the objects of the named paths (the seat git's trees with paths). An answer without the
|
||||
// object of every path asked is an error naming it.
|
||||
func namedVia(call toolCaller) func(ctx context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error) {
|
||||
return func(_ context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error) {
|
||||
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha, "paths": paths})
|
||||
a, err := treesOf(call, repository, git.TreesInput{Commit: sha, Paths: paths})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a struct {
|
||||
Paths map[string]map[string]string `json:"paths"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the answer of %s is not {root, trees, paths}: %w", commitTreesTool, err)
|
||||
}
|
||||
for dir, want := range paths {
|
||||
for _, p := range want {
|
||||
if a.Paths[dir][p] == "" {
|
||||
return nil, fmt.Errorf("the answer of %s names no object for %q of %q", commitTreesTool, p, dir)
|
||||
return nil, fmt.Errorf("the answer of %s names no object for %q of %q", gitVerb(git.VerbTrees), p, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,24 +95,14 @@ func namedVia(call toolCaller) func(ctx context.Context, repository, sha string,
|
||||
}
|
||||
}
|
||||
|
||||
// trunkVia asks the forge for the module directories of a commit (issue 1900): the keys of the `trees` the same tool
|
||||
// answers without paths, one per directory that holds a module.json (the root as ""; the tool lists no other).
|
||||
// trunkVia asks the forge for the module directories of a commit (issue 1900): the keys of the trees the seat git answers
|
||||
// without paths, one per directory that holds a module.json (the root as "").
|
||||
func trunkVia(call toolCaller) func(ctx context.Context, repository, sha string) ([]string, error) {
|
||||
return func(_ context.Context, repository, sha string) ([]string, error) {
|
||||
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha})
|
||||
a, err := treesOf(call, repository, git.TreesInput{Commit: sha})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a struct {
|
||||
Trees map[string]string `json:"trees"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the answer of %s is not {root, trees}: %w", commitTreesTool, err)
|
||||
}
|
||||
dirs := make([]string, 0, len(a.Trees))
|
||||
for dir := range a.Trees {
|
||||
dirs = append(dirs, dir)
|
||||
@@ -82,36 +111,25 @@ func trunkVia(call toolCaller) func(ctx context.Context, repository, sha string)
|
||||
}
|
||||
}
|
||||
|
||||
// filesVia asks the forge for every blob under each directory at a commit (the same tool with `files`, ADR 0593 §2): it
|
||||
// answers {files} by directory, then path relative to it, each with its blob's object id. An answer that lists no
|
||||
// directory asked is an error naming it; a cut listing fails the call in the tool.
|
||||
// filesVia asks the forge for every blob under each directory at a commit (the seat git's trees with files, ADR 0593 §2),
|
||||
// by directory, then path relative to it. An answer that lists no directory asked is an error naming it.
|
||||
func filesVia(call toolCaller) func(ctx context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error) {
|
||||
return func(_ context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error) {
|
||||
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha, "files": dirs})
|
||||
a, err := treesOf(call, repository, git.TreesInput{Commit: sha, Files: dirs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var a struct {
|
||||
Files map[string]map[string]string `json:"files"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return nil, fmt.Errorf("the answer of %s is not {files}: %w", commitTreesTool, err)
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if _, ok := a.Files[d]; !ok {
|
||||
return nil, fmt.Errorf("the answer of %s lists no files of %q: %w", commitTreesTool, d, index.ErrNotInTree)
|
||||
return nil, fmt.Errorf("the answer of %s lists no files of %q: %w", gitVerb(git.VerbTrees), d, index.ErrNotInTree)
|
||||
}
|
||||
}
|
||||
return a.Files, nil
|
||||
}
|
||||
}
|
||||
|
||||
// goSumVia reads a module directory's go.sum at a commit (gitea_get_file): the version line's packages of a module
|
||||
// whose build reported no imports (ADR 0593 §2). A directory with no go.sum answers nothing.
|
||||
// goSumVia reads a module directory's go.sum at a commit: the version line's packages of a module whose build reported
|
||||
// no imports (ADR 0593 §2). A directory with no go.sum answers nothing.
|
||||
func goSumVia(call toolCaller) func(ctx context.Context, repository, sha, dir string) ([]byte, error) {
|
||||
return goFileVia(call, "go.sum")
|
||||
}
|
||||
@@ -122,14 +140,10 @@ func goModVia(call toolCaller) func(ctx context.Context, repository, sha, dir st
|
||||
return goFileVia(call, "go.mod")
|
||||
}
|
||||
|
||||
// goFileVia reads one Go file of a module directory at a commit through the forge's get-file tool; a file the commit does
|
||||
// not hold is none.
|
||||
// goFileVia reads one Go file of a module directory at a commit through the seat git's file; a file the forge says the
|
||||
// commit does not hold is none.
|
||||
func goFileVia(call toolCaller, name string) func(ctx context.Context, repository, sha, dir string) ([]byte, error) {
|
||||
return func(_ context.Context, repository, sha, dir string) ([]byte, error) {
|
||||
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
p := name
|
||||
switch {
|
||||
case name == "": // readFileVia: dir is the file's whole path
|
||||
@@ -137,107 +151,69 @@ func goFileVia(call toolCaller, name string) func(ctx context.Context, repositor
|
||||
case dir != "" && dir != meshcatalogclient.RootPath:
|
||||
p = dir + "/" + name
|
||||
}
|
||||
raw, err := call(getFileTool, map[string]string{"owner": owner, "repo": repo, "path": p, "ref": sha})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), ": 404 ") {
|
||||
return nil, nil
|
||||
}
|
||||
content, absent, err := fileOf(call, repository, p, sha)
|
||||
if err != nil || absent {
|
||||
return nil, err
|
||||
}
|
||||
var f struct {
|
||||
File *struct {
|
||||
Content *string `json:"content"`
|
||||
} `json:"file"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &f); err != nil || f.File == nil || f.File.Content == nil {
|
||||
return nil, fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err)
|
||||
}
|
||||
return []byte(*f.File.Content), nil
|
||||
return content, nil
|
||||
}
|
||||
}
|
||||
|
||||
// containsVia asks the forge whether a commit is in another (gitea.gitea_contains: contains is true for the same commit or an ancestor).
|
||||
// containsVia asks the forge whether a commit is in another (the seat git's contains: true for the same commit or an
|
||||
// ancestor).
|
||||
func containsVia(call toolCaller) func(ctx context.Context, repository, commit, in string) (bool, error) {
|
||||
return func(_ context.Context, repository, commit, in string) (bool, error) {
|
||||
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
|
||||
if !ok {
|
||||
r := trunk.RepositoryName(repository)
|
||||
if !strings.Contains(r, "/") {
|
||||
return false, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
raw, err := call("gitea.gitea_contains", map[string]any{"owner": owner, "repo": repo, "commit": commit, "in": in})
|
||||
if err != nil {
|
||||
var a git.ContainsAnswer
|
||||
if err := askGit(call, git.VerbContains, git.ContainsInput{Repository: r, Commit: commit, In: in}, &a); err != nil {
|
||||
return false, err
|
||||
}
|
||||
var a struct {
|
||||
Contains *bool `json:"contains"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Contains == nil {
|
||||
return false, fmt.Errorf("the answer of gitea.gitea_contains names no contains: %v", err)
|
||||
}
|
||||
return *a.Contains, nil
|
||||
return a.Contains, nil
|
||||
}
|
||||
}
|
||||
|
||||
// getFileTool is the forge's tool for one file at a ref (mesh-catalog modules/gitea gitea_get_file): arguments owner,
|
||||
// repo, path and ref, answer {file: {content}}; a path it does not have fails with a 404 in its words.
|
||||
const getFileTool = "gitea.gitea_get_file"
|
||||
|
||||
// lockFiles are read beside a manifest, as the forge bridge reads them (bridge.LockFiles).
|
||||
var lockFiles = []string{"go.sum", "package-lock.json"}
|
||||
|
||||
// readVia reads a repository at a commit as the forge bridge does for a merged message: the trees by gitea_commit_trees,
|
||||
// then the module.json and lock files of the one directory dir by gitea_get_file (a repository can hold hundreds). A manifest the
|
||||
// forge does not have is an error; a directory with no module.json in the trees is answered with no manifest; a lock file it does not have is left out.
|
||||
// readVia reads a repository at a commit as the forge bridge does for a merged message: the seat git's trees, then the
|
||||
// module.json and lock files of the one directory dir by its file (a repository can hold hundreds). A manifest the forge
|
||||
// does not have is an error; a directory with no module.json in the trees is answered with no manifest; a lock file it does
|
||||
// not have is left out.
|
||||
func readVia(call toolCaller) func(ctx context.Context, repository, sha, dir string) (index.RepoRead, error) {
|
||||
return func(_ context.Context, repository, sha, dir string) (index.RepoRead, error) {
|
||||
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
|
||||
if !ok {
|
||||
return index.RepoRead{}, fmt.Errorf("%q is not <owner>/<name>", repository)
|
||||
}
|
||||
raw, err := call(commitTreesTool, map[string]any{"owner": owner, "repo": repo, "sha": sha})
|
||||
a, err := treesOf(call, repository, git.TreesInput{Commit: sha})
|
||||
if err != nil {
|
||||
return index.RepoRead{}, err
|
||||
}
|
||||
var a struct {
|
||||
Root string `json:"root"`
|
||||
Trees map[string]string `json:"trees"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return index.RepoRead{}, fmt.Errorf("the answer of %s is not {root, trees}: %w", commitTreesTool, err)
|
||||
}
|
||||
out := index.RepoRead{Root: a.Root, Trees: a.Trees, Manifests: map[string]string{}, Locks: map[string]map[string]string{}}
|
||||
file := func(dir, name string) (string, error) {
|
||||
file := func(dir, name string) (string, bool, error) {
|
||||
p := name
|
||||
if dir != "" {
|
||||
p = dir + "/" + name
|
||||
}
|
||||
raw, err := call(getFileTool, map[string]string{"owner": owner, "repo": repo, "path": p, "ref": sha})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var f struct {
|
||||
File *struct {
|
||||
Content *string `json:"content"`
|
||||
} `json:"file"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &f); err != nil || f.File == nil || f.File.Content == nil {
|
||||
return "", fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err)
|
||||
}
|
||||
return *f.File.Content, nil
|
||||
content, absent, err := fileOf(call, repository, p, sha)
|
||||
return string(content), absent, err
|
||||
}
|
||||
if _, held := a.Trees[dir]; held {
|
||||
m, err := file(dir, "module.json")
|
||||
m, absent, err := file(dir, "module.json")
|
||||
if err == nil && absent {
|
||||
err = fmt.Errorf("the forge holds no such file at %.12s", sha)
|
||||
}
|
||||
if err != nil {
|
||||
return index.RepoRead{}, fmt.Errorf("%s: %w", path.Join(dir, "module.json"), err)
|
||||
}
|
||||
out.Manifests[dir] = m
|
||||
for _, name := range lockFiles {
|
||||
c, err := file(dir, name)
|
||||
c, absent, err := file(dir, name)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), ": 404 ") {
|
||||
continue
|
||||
}
|
||||
return index.RepoRead{}, fmt.Errorf("%s: %w", path.Join(dir, name), err)
|
||||
}
|
||||
if absent {
|
||||
continue
|
||||
}
|
||||
sum := sha256.Sum256([]byte(c))
|
||||
if out.Locks[dir] == nil {
|
||||
out.Locks[dir] = map[string]string{}
|
||||
@@ -257,34 +233,24 @@ func readVia(call toolCaller) func(ctx context.Context, repository, sha, dir str
|
||||
}
|
||||
}
|
||||
|
||||
// The forge's tool for the open pull requests, and the trunk branch they are open into.
|
||||
const (
|
||||
openPullsTool = "gitea.gitea_open_pulls"
|
||||
trunkBranch = "main"
|
||||
)
|
||||
// trunkBranch is the branch pull requests are open into.
|
||||
const trunkBranch = "main"
|
||||
|
||||
// openVia asks the forge which pull requests into the trunk are open now (gitea_open_pulls), every repository's.
|
||||
// openVia asks the forge which pull requests into the trunk are open now (the seat git's open-pulls), every repository's.
|
||||
// An answer that lacks the list is refused, never read as none.
|
||||
func openVia(call toolCaller) func(ctx context.Context) ([]forgemsg.OpenPull, error) {
|
||||
return func(context.Context) ([]forgemsg.OpenPull, error) {
|
||||
raw, err := call(openPullsTool, map[string]any{})
|
||||
if err != nil {
|
||||
var o git.OpenPullsAnswer
|
||||
if err := askGit(call, git.VerbOpenPulls, git.OpenPullsInput{}, &o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var o struct {
|
||||
Pulls *[]struct {
|
||||
Owner string `json:"owner"`
|
||||
Repo string `json:"repo"`
|
||||
Number int `json:"number"`
|
||||
Base string `json:"base"`
|
||||
} `json:"pulls"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &o); err != nil || o.Pulls == nil {
|
||||
return nil, fmt.Errorf("the answer of %s is not {pulls}: %v", openPullsTool, err)
|
||||
if o.Pulls == nil {
|
||||
return nil, fmt.Errorf("the answer of %s lists no pulls", gitVerb(git.VerbOpenPulls))
|
||||
}
|
||||
out := []forgemsg.OpenPull{}
|
||||
for _, p := range *o.Pulls {
|
||||
if p.Base == trunkBranch && p.Number > 0 {
|
||||
out = append(out, forgemsg.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number})
|
||||
for _, p := range o.Pulls {
|
||||
if p.Pull.Base == trunkBranch && p.Pull.Number > 0 {
|
||||
out = append(out, forgemsg.OpenPull{Repository: p.Pull.Repository, Number: int(p.Pull.Number)})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
|
||||
"mesh-delivery-indexer/internal/index"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -11,20 +12,48 @@ import (
|
||||
)
|
||||
|
||||
func TestNamedViaAsksTheTreesToolWithThePathsAndTakesTheirObjects(t *testing.T) { // I6, ADR 0499 clause 3
|
||||
var asked map[string]any
|
||||
var asked git.TreesInput
|
||||
call := func(key string, body any) (json.RawMessage, error) {
|
||||
if key != commitTreesTool {
|
||||
if key != "seat:git.trees" {
|
||||
t.Errorf("called %s", key)
|
||||
}
|
||||
asked = body.(map[string]any)
|
||||
asked = body.(git.TreesInput)
|
||||
return json.RawMessage(`{"root":"r0","trees":{"":"r0"},"paths":{"":{"src":"1111111111111111111111111111111111111111","package.json":"3333333333333333333333333333333333333333"}}}`), nil
|
||||
}
|
||||
got, err := namedVia(call)(context.Background(), "novox/mesh-sdk", "s", map[string][]string{"": {"package.json", "src"}})
|
||||
if err != nil || got[""]["src"] != "1111111111111111111111111111111111111111" || got[""]["package.json"] == "" {
|
||||
t.Fatalf("objects = %v, %v", got, err)
|
||||
}
|
||||
if asked["owner"] != "novox" || asked["repo"] != "mesh-sdk" || asked["sha"] != "s" || asked["paths"] == nil {
|
||||
t.Errorf("asked %v, want the owner, repo, commit and paths", asked)
|
||||
if asked.Repository != "novox/mesh-sdk" || asked.Commit != "s" || asked.Paths == nil {
|
||||
t.Errorf("asked %v, want the repository, commit and paths", asked)
|
||||
}
|
||||
// An answer that carries the seat's error is the failure it names, never an empty answer.
|
||||
failed := func(string, any) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"error":"the forge did not answer","error-kind":"failed"}`), nil
|
||||
}
|
||||
if _, err := namedVia(failed)(context.Background(), "novox/mesh-sdk", "s", nil); err == nil || !strings.Contains(err.Error(), "the forge did not answer") {
|
||||
t.Errorf("an error answer: %v, want its failure", err)
|
||||
}
|
||||
for _, row := range []struct {
|
||||
name, answer string
|
||||
want int
|
||||
refused bool
|
||||
}{
|
||||
{"one open into the trunk, one into another branch", `{"pulls":[{"pull":{"repository":"novox/a","number":3,"base":"main","branch":"b","head":"h"}},{"pull":{"repository":"novox/a","number":4,"base":"dev","branch":"c","head":"h"}}]}`, 1, false},
|
||||
{"none open", `{"pulls":[]}`, 0, false},
|
||||
{"no list", `{}`, 0, true},
|
||||
{"an error", `{"error":"refused","error-kind":"refused"}`, 0, true},
|
||||
} {
|
||||
open := func(key string, _ any) (json.RawMessage, error) {
|
||||
if key != "seat:git.open-pulls" {
|
||||
t.Errorf("called %s", key)
|
||||
}
|
||||
return json.RawMessage(row.answer), nil
|
||||
}
|
||||
got, err := openVia(open)(context.Background())
|
||||
if (err != nil) != row.refused || len(got) != row.want {
|
||||
t.Errorf("open-pulls %s: %v %v, want %d, refused %v", row.name, got, err, row.want, row.refused)
|
||||
}
|
||||
}
|
||||
call = func(string, any) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"root":"r0","trees":{},"paths":{"":{"src":"1111111111111111111111111111111111111111"}}}`), nil
|
||||
@@ -48,7 +77,7 @@ func TestTheTrunkIsTheDirectoriesTheForgeListsTrees(t *testing.T) { // 1900/AC3
|
||||
if !slices.Equal(dirs, []string{"modules/a", "modules/b"}) {
|
||||
t.Errorf("dirs = %v", dirs)
|
||||
}
|
||||
if _, has := gotArgs.(map[string]any)["paths"]; has {
|
||||
if gotArgs.(git.TreesInput).Paths != nil {
|
||||
t.Error("the trunk asked for paths")
|
||||
}
|
||||
// A directory the answer lists no files of is a failure of the tree (index.ErrNotInTree); a call that fails is not.
|
||||
@@ -114,17 +143,17 @@ func TestReadViaReadsManifestsAndLocks(t *testing.T) {
|
||||
var files []string
|
||||
call := func(key string, body any) (json.RawMessage, error) {
|
||||
switch key {
|
||||
case commitTreesTool:
|
||||
case "seat:git.trees":
|
||||
return json.RawMessage(`{"root":"r0","trees":{"":"t1","modules/other":"t2"}}`), nil
|
||||
case getFileTool:
|
||||
files = append(files, body.(map[string]string)["path"])
|
||||
switch body.(map[string]string)["path"] {
|
||||
case "seat:git.file":
|
||||
files = append(files, body.(git.FileInput).Path)
|
||||
switch body.(git.FileInput).Path {
|
||||
case "module.json":
|
||||
return json.RawMessage(`{"file":{"content":"{\"module\":\"snake\"}"}}`), nil
|
||||
return json.RawMessage(`{"path":"module.json","content":"{\"module\":\"snake\"}"}`), nil
|
||||
case "go.sum":
|
||||
return json.RawMessage(`{"file":{"content":"x"}}`), nil
|
||||
return json.RawMessage(`{"path":"go.sum","content":"x"}`), nil
|
||||
}
|
||||
return nil, errors.New("Gitea API /x: 404 not found")
|
||||
return json.RawMessage(`{"path":"x","absent":true}`), nil
|
||||
}
|
||||
return nil, errors.New("unexpected " + key)
|
||||
}
|
||||
|
||||
+23
-28
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -419,70 +420,64 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
// seatFiles are mesh-contracts' files by commit, as gitea_get_file answers them.
|
||||
// seatFiles are mesh-contracts' files by commit, as the seat git's file answers them.
|
||||
var seatFiles = map[string]map[string]string{}
|
||||
|
||||
// serveForge answers the forge's tools on the bus as the gitea module does: gitea_commit_trees (the trees of a commit,
|
||||
// or with files the blobs under each directory of mesh-sdk's listing at that commit), gitea_get_file (every go.sum
|
||||
// holds goSum) and gitea_contains.
|
||||
// serveForge answers the seat git's verbs on the bus as its holder, the forge module, does (milestone 47/G W4.15): trees
|
||||
// (the trees of a commit, or with files the blobs under each directory of mesh-sdk's listing at that commit), file (every
|
||||
// go.sum holds goSum; a seat definition the commit does not hold is absent) and contains.
|
||||
func serveForge(t *testing.T, nc *nats.Conn, listing map[string]map[string]string, goSum string) {
|
||||
t.Helper()
|
||||
reply := func(m *nats.Msg, result any) {
|
||||
raw, _ := json.Marshal(map[string]any{"result": result, "node": "n0"})
|
||||
_ = m.Respond(raw)
|
||||
}
|
||||
trees, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_commit_trees", func(m *nats.Msg) {
|
||||
var a struct {
|
||||
Repo string `json:"repo"`
|
||||
Sha string `json:"sha"`
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
trees, err := nc.Subscribe("mesh.seat.git.tool.trees", func(m *nats.Msg) {
|
||||
var a git.TreesInput
|
||||
_ = json.Unmarshal(m.Data, &a)
|
||||
out := map[string]any{"root": "r0", "trees": map[string]string{}}
|
||||
out := git.TreesAnswer{Root: "r0", Trees: map[string]string{}, Commit: a.Commit}
|
||||
if len(a.Files) > 0 {
|
||||
files := map[string]map[string]string{}
|
||||
out.Files = map[string]map[string]string{}
|
||||
for _, d := range a.Files {
|
||||
if a.Repo == "mesh-sdk" && d == "go" && listing[a.Sha] != nil {
|
||||
files[d] = listing[a.Sha]
|
||||
if a.Repository == "novox/mesh-sdk" && d == "go" && listing[a.Commit] != nil {
|
||||
out.Files[d] = listing[a.Commit]
|
||||
}
|
||||
if fs, ok := seatFiles[a.Sha]; ok && strings.HasPrefix(d, "seats") { // a contracts module's seats/ directory (D-I5)
|
||||
files[d] = map[string]string{}
|
||||
if fs, ok := seatFiles[a.Commit]; ok && strings.HasPrefix(d, "seats") { // a contracts module's seats/ directory (D-I5)
|
||||
out.Files[d] = map[string]string{}
|
||||
for p := range fs {
|
||||
if rel, ok := strings.CutPrefix(p, d+"/"); ok {
|
||||
files[d][rel] = "blob"
|
||||
out.Files[d][rel] = "blob"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out["files"] = files
|
||||
}
|
||||
reply(m, out)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_get_file", func(m *nats.Msg) {
|
||||
var a struct{ Repo, Path, Ref string }
|
||||
file, err := nc.Subscribe("mesh.seat.git.tool.file", func(m *nats.Msg) {
|
||||
var a git.FileInput
|
||||
_ = json.Unmarshal(m.Data, &a)
|
||||
if _, held := seatFiles[a.Ref]; held { // the seat definitions (DLV-I17): a path the commit does not hold is a 404
|
||||
if _, held := seatFiles[a.Ref]; held { // the seat definitions (DLV-I17): a path the commit does not hold is absent
|
||||
if v, ok := seatFiles[a.Ref][a.Path]; ok {
|
||||
reply(m, map[string]any{"file": map[string]any{"content": v}})
|
||||
reply(m, git.FileAnswer{Path: a.Path, Ref: a.Ref, Content: v})
|
||||
} else {
|
||||
raw, _ := json.Marshal(map[string]any{"error": "gitea: 404 Not Found", "node": "n0"})
|
||||
_ = m.Respond(raw)
|
||||
reply(m, git.FileAnswer{Path: a.Path, Ref: a.Ref, Absent: true})
|
||||
}
|
||||
return
|
||||
}
|
||||
reply(m, map[string]any{"file": map[string]any{"content": goSum}})
|
||||
reply(m, git.FileAnswer{Path: a.Path, Ref: a.Ref, Content: goSum})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Each merge here is newer than the one before: a commit is in another only when it is that commit.
|
||||
contains, err := nc.Subscribe("mesh.mod.gitea.tool.gitea_contains", func(m *nats.Msg) {
|
||||
var a struct{ Commit, In string }
|
||||
contains, err := nc.Subscribe("mesh.seat.git.tool.contains", func(m *nats.Msg) {
|
||||
var a git.ContainsInput
|
||||
_ = json.Unmarshal(m.Data, &a)
|
||||
reply(m, map[string]any{"contains": a.Commit == a.In})
|
||||
reply(m, git.ContainsAnswer{Commit: a.Commit, In: a.In, Contains: a.Commit == a.In})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// checkerAsksStream is the checker's stream asks as the bus names it (its manifest's streams; StreamBusName).
|
||||
const checkerAsksStream = "ASKS"
|
||||
|
||||
// readFileVia reads one file of a repository at a commit through the forge's get-file tool; a path the commit does not
|
||||
// readFileVia reads one file of a repository at a commit through the seat git's file; a path the commit does not
|
||||
// hold is nil with no error.
|
||||
func readFileVia(call toolCaller) func(ctx context.Context, repository, sha, path string) ([]byte, error) {
|
||||
return goFileVia(call, "")
|
||||
|
||||
@@ -10,6 +10,7 @@ require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/git v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -92,7 +92,7 @@ type Indexer struct {
|
||||
Packages Store
|
||||
// SeatDefinitions is the state seat-definitions (seatdefs.go; design 26, DLV-I17); nil keeps none.
|
||||
SeatDefinitions Store
|
||||
// ReadFile reads one file of a repository at a commit (gitea.gitea_get_file); a file the commit does not hold is
|
||||
// ReadFile reads one file of a repository at a commit (the seat git's file); a file the commit does not hold is
|
||||
// nil with no error. Nil reads no seat definition.
|
||||
ReadFile func(ctx context.Context, repository, sha, path string) ([]byte, error)
|
||||
// SaySeatDefinition says seat-definition-changed (seatdefs.go); nil says nothing.
|
||||
@@ -108,13 +108,13 @@ type Indexer struct {
|
||||
// Graph is the module graph's state (GraphBucket, graph.go; novox/hq ADR 0603 §2); nil writes no graph.
|
||||
Graph Store
|
||||
// Files asks the forge for every blob under each directory at a commit, by directory then path relative to it, each
|
||||
// with its git object id (gitea.gitea_commit_trees with files): the listing a package's digest is computed from.
|
||||
// with its git object id (the seat git's trees with files): the listing a package's digest is computed from.
|
||||
// Nil publishes no package.
|
||||
Files func(ctx context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error)
|
||||
// GoSum reads a module directory's go.sum at a commit (gitea.gitea_get_file): the mesh Go modules a module with no
|
||||
// GoSum reads a module directory's go.sum at a commit (the seat git's file): the mesh Go modules a module with no
|
||||
// reported import set takes the version line of. Nil reads none: such a module keeps what it held.
|
||||
GoSum func(ctx context.Context, repository, sha, dir string) ([]byte, error)
|
||||
// GoMod reads a module directory's go.mod at a commit (gitea.gitea_get_file): the mesh Go modules of its own
|
||||
// GoMod reads a module directory's go.mod at a commit (the seat git's file): the mesh Go modules of its own
|
||||
// repository it replaces by a directory (fingerprint.ReplacedSiblings), which the version line takes by their
|
||||
// content; nil reads none.
|
||||
GoMod func(ctx context.Context, repository, sha, dir string) ([]byte, error)
|
||||
@@ -122,7 +122,7 @@ type Indexer struct {
|
||||
Now func() time.Time
|
||||
Builds *Builds
|
||||
// Named asks the forge for the git object of each named path of the module directories given, at a commit
|
||||
// (gitea.gitea_commit_trees with paths; novox/hq ADR 0499 clauses 2 and 3): the indexer is the one reader of a
|
||||
// (the seat git's trees with paths; novox/hq ADR 0499 clauses 2 and 3): the indexer is the one reader of a
|
||||
// manifest's paths (ADR 0495 clause 1, the lead's decision L8 of R4e). Nil asks nothing, and a module naming paths
|
||||
// has none.
|
||||
Named func(ctx context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error)
|
||||
@@ -130,10 +130,10 @@ type Indexer struct {
|
||||
// Read asks the forge for a repository's trees at a commit and the manifest and lock-file hashes of the one module directory dir (the
|
||||
// root spelled ""), as the forge bridge reads them for a merged message (registered.go). Nil indexes no registered module.
|
||||
Read func(ctx context.Context, repository, sha, dir string) (RepoRead, error)
|
||||
// Contains says whether a commit is the second or an ancestor of it (gitea.gitea_contains).
|
||||
// Contains says whether a commit is the second or an ancestor of it (the seat git's contains).
|
||||
Contains func(ctx context.Context, repository, commit, in string) (bool, error)
|
||||
Logf func(format string, args ...any)
|
||||
// ReadOpen asks the forge which pull requests into the trunk are open now (gitea_open_pulls). A catalogue pull message
|
||||
// ReadOpen asks the forge which pull requests into the trunk are open now (the seat git's open-pulls). A catalogue pull message
|
||||
// that finds the candidates of another pull request asks it and deletes those of a pull request no longer open, so a
|
||||
// closed message the indexer missed heals at the next one (issue 1126); nil asks none.
|
||||
ReadOpen func(ctx context.Context) ([]forgemsg.OpenPull, error)
|
||||
|
||||
@@ -53,10 +53,10 @@ func newMergeWrite(f forgemsg.Forge) *mergeWrite {
|
||||
return &mergeWrite{repository: f.Repository, merge: f.Merge}
|
||||
}
|
||||
|
||||
// notFound says the forge answered that a commit it was asked about does not exist: gitea's status ": 404 ", matched as
|
||||
// readVia matches it (cmd/mesh-delivery-indexer named.go). A bare "404" is not enough: the error's words carry both shas,
|
||||
// and a 502 naming a sha with 404 in it would read as not older and write a late message backwards. The thorough form,
|
||||
// a typed not-found answered by gitea_contains, is noted on issue 1126.
|
||||
// notFound says the forge answered that a commit it was asked about does not exist: the seat git's contains answers it as
|
||||
// an error of kind failed whose words carry the forge's status ": 404 " (cmd/mesh-delivery-indexer named.go). A bare "404"
|
||||
// is not enough: the error's words carry both shas, and a 502 naming a sha with 404 in it would read as not older and
|
||||
// write a late message backwards. The thorough form, a typed not-found in the seat's error kinds, is noted on issue 1126.
|
||||
func notFound(err error) bool { return err != nil && strings.Contains(err.Error(), ": 404 ") }
|
||||
|
||||
// older says commit is older than later: the forge says commit is in later and the two differ. A commit the forge does not
|
||||
|
||||
@@ -3,6 +3,7 @@ package manifest
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -169,7 +170,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
// Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths:
|
||||
// ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a
|
||||
// commit is in another (issue 1900).
|
||||
if want := []string{"mesh-delivery-assigner.assignments", "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence"}; !reflect.DeepEqual(m.Invokes, want) {
|
||||
if want := []string{"mesh-delivery-assigner.assignments", "seat:git.trees", "seat:git.file", "seat:git.contains", "seat:git.open-pulls", "mesh-delivery-checker.checker_tries", "mesh-delivery-forge-bridge.bridge_merged_message", "seat:node-engine.composition", "seat:mesh-delivery.source", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence"}; !reflect.DeepEqual(m.Invokes, want) {
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle
|
||||
@@ -385,14 +386,16 @@ func TestServesNoRequestSubjectAndHoldsNoTicker(t *testing.T) { // 1238/AC9 (ADR
|
||||
}
|
||||
}
|
||||
|
||||
// Every forge tool the program calls is granted in invokes, or the bus refuses the call (issue 1900).
|
||||
// Every forge read the program makes is a verb of the seat git granted in invokes, or the bus refuses the call (issue
|
||||
// 1900); no program file names the forge module's own tools (milestone 47/G W4.15, design 47 *The seat git*).
|
||||
func TestEveryGiteaToolTheCodeCallsIsInInvokes(t *testing.T) {
|
||||
m, _ := load(t)
|
||||
files, err := filepath.Glob("cmd/mesh-delivery-indexer/*.go")
|
||||
if err != nil || len(files) == 0 {
|
||||
t.Fatalf("no program files: %v", err)
|
||||
}
|
||||
re := regexp.MustCompile(`gitea\.gitea_[a-z_]+`)
|
||||
verbs := map[string]string{"VerbTrees": git.VerbTrees, "VerbFile": git.VerbFile, "VerbContains": git.VerbContains, "VerbOpenPulls": git.VerbOpenPulls}
|
||||
forge, asked := regexp.MustCompile(`gitea\.gitea_[a-z_]+`), regexp.MustCompile(`git\.Verb[A-Za-z]+`)
|
||||
seen := 0
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
@@ -402,14 +405,18 @@ func TestEveryGiteaToolTheCodeCallsIsInInvokes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tool := range re.FindAllString(string(src), -1) {
|
||||
for _, tool := range forge.FindAllString(string(src), -1) {
|
||||
t.Errorf("%s calls the forge module's own tool %s, not the seat git", f, tool)
|
||||
}
|
||||
for _, v := range asked.FindAllString(string(src), -1) {
|
||||
seen++
|
||||
if !slices.Contains(m.Invokes, tool) {
|
||||
t.Errorf("%s calls %s, which invokes does not grant", f, tool)
|
||||
verb, ok := verbs[strings.TrimPrefix(v, "git.")]
|
||||
if !ok || !slices.Contains(m.Invokes, "seat:"+git.Seat+"."+verb) {
|
||||
t.Errorf("%s asks %s, which invokes does not grant", f, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if seen < 2 {
|
||||
t.Errorf("found %d tool names in the program, want at least the two it calls", seen)
|
||||
if seen < 4 {
|
||||
t.Errorf("found %d verbs of the seat git in the program, want at least the four it asks", seen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +121,10 @@
|
||||
],
|
||||
"invokes": [
|
||||
"mesh-delivery-assigner.assignments",
|
||||
"gitea.gitea_commit_trees",
|
||||
"gitea.gitea_contains",
|
||||
"gitea.gitea_get_file",
|
||||
"gitea.gitea_open_pulls",
|
||||
"seat:git.trees",
|
||||
"seat:git.file",
|
||||
"seat:git.contains",
|
||||
"seat:git.open-pulls",
|
||||
"mesh-delivery-checker.checker_tries",
|
||||
"mesh-delivery-forge-bridge.bridge_merged_message",
|
||||
"seat:node-engine.composition",
|
||||
|
||||
Reference in New Issue
Block a user