Pass reference bindings to Notarius
This commit is contained in:
@@ -19,7 +19,7 @@ different contract.
|
|||||||
| 1 | Add the reference-selector and configuration vocabulary, including optional spell-catalog inputs. | Completed |
|
| 1 | Add the reference-selector and configuration vocabulary, including optional spell-catalog inputs. | Completed |
|
||||||
| 2 | Materialize and inventory the optional spell catalog through the prepare and operator lifecycle. | Completed |
|
| 2 | Materialize and inventory the optional spell catalog through the prepare and operator lifecycle. | Completed |
|
||||||
| 3 | Centralize manifest-authoritative prepared-input resolution and migrate analyze to it. | Completed |
|
| 3 | Centralize manifest-authoritative prepared-input resolution and migrate analyze to it. | Completed |
|
||||||
| 4 | Add deterministic Notarius v0.6 reference arguments at the subprocess adapter boundary. | Pending |
|
| 4 | Add deterministic Notarius v0.6 reference arguments at the subprocess adapter boundary. | Completed |
|
||||||
| 5 | Resolve references in extract and bind fingerprints, resume, and metadata to their identities. | Pending |
|
| 5 | Resolve references in extract and bind fingerprints, resume, and metadata to their identities. | Pending |
|
||||||
| 6 | Prove assembled extraction lifecycle and downstream invalidation behavior. | Pending |
|
| 6 | Prove assembled extraction lifecycle and downstream invalidation behavior. | Pending |
|
||||||
| 7 | Update canonical documentation and maintained examples for the completed feature. | Pending |
|
| 7 | Update canonical documentation and maintained examples for the completed feature. | Pending |
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ func (f *FakeRunner) Run(ctx context.Context, req RunRequest) (RunResult, error)
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return RunResult{}, err
|
return RunResult{}, err
|
||||||
}
|
}
|
||||||
f.Requests = append(f.Requests, req)
|
copyRequest := req
|
||||||
|
copyRequest.References = append([]ReferenceBinding(nil), req.References...)
|
||||||
|
f.Requests = append(f.Requests, copyRequest)
|
||||||
if f.Err != nil {
|
if f.Err != nil {
|
||||||
return RunResult{}, f.Err
|
return RunResult{}, f.Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ type Runner interface {
|
|||||||
Run(ctx context.Context, req RunRequest) (RunResult, error)
|
Run(ctx context.Context, req RunRequest) (RunResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReferenceBinding maps one normalized Notarius selector to an absolute
|
||||||
|
// external reference path.
|
||||||
|
type ReferenceBinding struct {
|
||||||
|
Selector string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
// RunRequest contains the resolved inputs and diagnostic destinations for one invocation.
|
// RunRequest contains the resolved inputs and diagnostic destinations for one invocation.
|
||||||
type RunRequest struct {
|
type RunRequest struct {
|
||||||
Binary string
|
Binary string
|
||||||
@@ -24,6 +31,7 @@ type RunRequest struct {
|
|||||||
ReceiptPath string
|
ReceiptPath string
|
||||||
LogPath string
|
LogPath string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
References []ReferenceBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receipt is the transport-neutral successful run receipt.
|
// Receipt is the transport-neutral successful run receipt.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/notariusref"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +48,8 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
|
|||||||
if r == nil || r.run == nil {
|
if r == nil || r.run == nil {
|
||||||
return RunResult{}, fmt.Errorf("notarius subprocess runner is nil")
|
return RunResult{}, fmt.Errorf("notarius subprocess runner is nil")
|
||||||
}
|
}
|
||||||
if err := validateRunRequest(req); err != nil {
|
references, err := validateRunRequest(req)
|
||||||
|
if err != nil {
|
||||||
return RunResult{}, err
|
return RunResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +58,11 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
|
|||||||
"--config", req.ConfigPath,
|
"--config", req.ConfigPath,
|
||||||
"--input", req.InputPath,
|
"--input", req.InputPath,
|
||||||
"--output-dir", req.OutputRoot,
|
"--output-dir", req.OutputRoot,
|
||||||
"--json",
|
|
||||||
}
|
}
|
||||||
|
for _, reference := range references {
|
||||||
|
args = append(args, "--reference", reference.Selector+"="+reference.Path)
|
||||||
|
}
|
||||||
|
args = append(args, "--json")
|
||||||
processResult, err := r.run(ctx, subprocess.RunRequest{
|
processResult, err := r.run(ctx, subprocess.RunRequest{
|
||||||
Executable: req.Binary,
|
Executable: req.Binary,
|
||||||
Args: args,
|
Args: args,
|
||||||
@@ -128,15 +133,15 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
|
|||||||
return baseResult, nil
|
return baseResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRunRequest(req RunRequest) error {
|
func validateRunRequest(req RunRequest) ([]ReferenceBinding, error) {
|
||||||
if strings.TrimSpace(req.Binary) == "" {
|
if strings.TrimSpace(req.Binary) == "" {
|
||||||
return fmt.Errorf("notarius binary is required")
|
return nil, fmt.Errorf("notarius binary is required")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.PipelineID) == "" {
|
if strings.TrimSpace(req.PipelineID) == "" {
|
||||||
return fmt.Errorf("notarius pipeline id is required")
|
return nil, fmt.Errorf("notarius pipeline id is required")
|
||||||
}
|
}
|
||||||
if req.Timeout <= 0 {
|
if req.Timeout <= 0 {
|
||||||
return fmt.Errorf("notarius timeout must be positive")
|
return nil, fmt.Errorf("notarius timeout must be positive")
|
||||||
}
|
}
|
||||||
for label, path := range map[string]string{
|
for label, path := range map[string]string{
|
||||||
"config": req.ConfigPath,
|
"config": req.ConfigPath,
|
||||||
@@ -147,34 +152,53 @@ func validateRunRequest(req RunRequest) error {
|
|||||||
"log": req.LogPath,
|
"log": req.LogPath,
|
||||||
} {
|
} {
|
||||||
if strings.TrimSpace(path) == "" {
|
if strings.TrimSpace(path) == "" {
|
||||||
return fmt.Errorf("notarius %s path is required", label)
|
return nil, fmt.Errorf("notarius %s path is required", label)
|
||||||
}
|
}
|
||||||
if !filepath.IsAbs(path) {
|
if !filepath.IsAbs(path) {
|
||||||
return fmt.Errorf("notarius %s path must be absolute", label)
|
return nil, fmt.Errorf("notarius %s path must be absolute", label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if filepath.Clean(req.ReceiptPath) == filepath.Clean(req.LogPath) {
|
if filepath.Clean(req.ReceiptPath) == filepath.Clean(req.LogPath) {
|
||||||
return fmt.Errorf("notarius receipt and log paths must be different")
|
return nil, fmt.Errorf("notarius receipt and log paths must be different")
|
||||||
|
}
|
||||||
|
references := make([]ReferenceBinding, 0, len(req.References))
|
||||||
|
selectors := make(map[string]struct{}, len(req.References))
|
||||||
|
for index, binding := range req.References {
|
||||||
|
selector, err := notariusref.NormalizeSelector(binding.Selector)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("notarius reference %d selector: %w", index, err)
|
||||||
|
}
|
||||||
|
if _, duplicate := selectors[selector]; duplicate {
|
||||||
|
return nil, fmt.Errorf("notarius reference selector %q is duplicated", selector)
|
||||||
|
}
|
||||||
|
selectors[selector] = struct{}{}
|
||||||
|
if strings.TrimSpace(binding.Path) == "" {
|
||||||
|
return nil, fmt.Errorf("notarius reference %q path is required", selector)
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(binding.Path) {
|
||||||
|
return nil, fmt.Errorf("notarius reference %q path must be absolute", selector)
|
||||||
|
}
|
||||||
|
references = append(references, ReferenceBinding{Selector: selector, Path: binding.Path})
|
||||||
}
|
}
|
||||||
if err := requireRegularFile(req.ConfigPath); err != nil {
|
if err := requireRegularFile(req.ConfigPath); err != nil {
|
||||||
return fmt.Errorf("validate notarius config path: %w", err)
|
return nil, fmt.Errorf("validate notarius config path: %w", err)
|
||||||
}
|
}
|
||||||
if err := requireRegularFile(req.InputPath); err != nil {
|
if err := requireRegularFile(req.InputPath); err != nil {
|
||||||
return fmt.Errorf("validate notarius input path: %w", err)
|
return nil, fmt.Errorf("validate notarius input path: %w", err)
|
||||||
}
|
}
|
||||||
if err := requireDirectory(req.OutputRoot); err != nil {
|
if err := requireDirectory(req.OutputRoot); err != nil {
|
||||||
return fmt.Errorf("validate notarius output root: %w", err)
|
return nil, fmt.Errorf("validate notarius output root: %w", err)
|
||||||
}
|
}
|
||||||
if err := requireDirectory(req.WorkingDirectory); err != nil {
|
if err := requireDirectory(req.WorkingDirectory); err != nil {
|
||||||
return fmt.Errorf("validate notarius working directory: %w", err)
|
return nil, fmt.Errorf("validate notarius working directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := validateLogDestination(req.ReceiptPath); err != nil {
|
if err := validateLogDestination(req.ReceiptPath); err != nil {
|
||||||
return fmt.Errorf("validate notarius receipt path: %w", err)
|
return nil, fmt.Errorf("validate notarius receipt path: %w", err)
|
||||||
}
|
}
|
||||||
if err := validateLogDestination(req.LogPath); err != nil {
|
if err := validateLogDestination(req.LogPath); err != nil {
|
||||||
return fmt.Errorf("validate notarius log path: %w", err)
|
return nil, fmt.Errorf("validate notarius log path: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return references, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type receiptDocument struct {
|
type receiptDocument struct {
|
||||||
|
|||||||
@@ -76,6 +76,101 @@ func TestSubprocessRunnerBuildsExactInvocationAndDiscoversBundle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerBuildsOrderedReferenceArguments(t *testing.T) {
|
||||||
|
req := validRunRequest(t)
|
||||||
|
referenceRoot := t.TempDir()
|
||||||
|
req.References = []ReferenceBinding{
|
||||||
|
{Selector: " party ", Path: filepath.Join(referenceRoot, "party context=primary.json")},
|
||||||
|
{Selector: " npc-registry . extract . glossary ", Path: filepath.Join(referenceRoot, "glossary.json")},
|
||||||
|
}
|
||||||
|
originalReferences := append([]ReferenceBinding(nil), req.References...)
|
||||||
|
var captured sharedsubprocess.RunRequest
|
||||||
|
runner := &SubprocessRunner{run: func(_ context.Context, processReq sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) {
|
||||||
|
captured = processReq
|
||||||
|
writeValidBundleAndReceipt(t, req, false)
|
||||||
|
return sharedsubprocess.RunResult{ExitCode: 0}, nil
|
||||||
|
}}
|
||||||
|
|
||||||
|
if _, err := runner.Run(context.Background(), req); err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
|
}
|
||||||
|
wantArgs := []string{
|
||||||
|
"run", req.PipelineID,
|
||||||
|
"--config", req.ConfigPath,
|
||||||
|
"--input", req.InputPath,
|
||||||
|
"--output-dir", req.OutputRoot,
|
||||||
|
"--reference", "party=" + req.References[0].Path,
|
||||||
|
"--reference", "npc-registry.extract.glossary=" + req.References[1].Path,
|
||||||
|
"--json",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(captured.Args, wantArgs) {
|
||||||
|
t.Fatalf("subprocess args = %#v, want %#v", captured.Args, wantArgs)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(req.References, originalReferences) {
|
||||||
|
t.Fatalf("Run() mutated caller references = %#v, want %#v", req.References, originalReferences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerRejectsInvalidReferencesBeforeLaunch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
references func(string) []ReferenceBinding
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid selector",
|
||||||
|
references: func(root string) []ReferenceBinding {
|
||||||
|
return []ReferenceBinding{{Selector: "lane.prepare.party", Path: filepath.Join(root, "party.json")}}
|
||||||
|
},
|
||||||
|
wantErr: "selector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate normalized selector",
|
||||||
|
references: func(root string) []ReferenceBinding {
|
||||||
|
return []ReferenceBinding{
|
||||||
|
{Selector: "lane.party", Path: filepath.Join(root, "party.json")},
|
||||||
|
{Selector: " lane . party ", Path: filepath.Join(root, "party-2.json")},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantErr: "duplicated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty path",
|
||||||
|
references: func(string) []ReferenceBinding {
|
||||||
|
return []ReferenceBinding{{Selector: "party", Path: " "}}
|
||||||
|
},
|
||||||
|
wantErr: "path is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative path",
|
||||||
|
references: func(string) []ReferenceBinding {
|
||||||
|
return []ReferenceBinding{{Selector: "party", Path: "references/party.json"}}
|
||||||
|
},
|
||||||
|
wantErr: "path must be absolute",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := validRunRequest(t)
|
||||||
|
req.References = tt.references(t.TempDir())
|
||||||
|
started := false
|
||||||
|
runner := &SubprocessRunner{run: func(context.Context, sharedsubprocess.RunRequest) (sharedsubprocess.RunResult, error) {
|
||||||
|
started = true
|
||||||
|
return sharedsubprocess.RunResult{}, nil
|
||||||
|
}}
|
||||||
|
|
||||||
|
_, err := runner.Run(context.Background(), req)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("Run() error = %v, want containing %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if started {
|
||||||
|
t.Fatal("subprocess started after request validation failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubprocessRunnerUsesMinimalEnvironmentAndSeparatesStreams(t *testing.T) {
|
func TestSubprocessRunnerUsesMinimalEnvironmentAndSeparatesStreams(t *testing.T) {
|
||||||
req := validRunRequest(t)
|
req := validRunRequest(t)
|
||||||
writeValidBundleAndReceipt(t, req, false)
|
writeValidBundleAndReceipt(t, req, false)
|
||||||
@@ -432,13 +527,17 @@ func TestLoadDiagnosticSummariesValidateBoundsAndTolerateUnknownFields(t *testin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFakeRunnerCapturesRequestsAndHonorsContextAndError(t *testing.T) {
|
func TestFakeRunnerCapturesRequestsAndHonorsContextAndError(t *testing.T) {
|
||||||
req := RunRequest{PipelineID: "pipeline"}
|
req := RunRequest{PipelineID: "pipeline", References: []ReferenceBinding{{Selector: "party", Path: "/references/party.json"}}}
|
||||||
want := RunResult{BundleRoot: "/bundle"}
|
want := RunResult{BundleRoot: "/bundle"}
|
||||||
fake := &FakeRunner{Result: want}
|
fake := &FakeRunner{Result: want}
|
||||||
got, err := fake.Run(context.Background(), req)
|
got, err := fake.Run(context.Background(), req)
|
||||||
if err != nil || !reflect.DeepEqual(got, want) || !reflect.DeepEqual(fake.Requests, []RunRequest{req}) {
|
if err != nil || !reflect.DeepEqual(got, want) || !reflect.DeepEqual(fake.Requests, []RunRequest{req}) {
|
||||||
t.Fatalf("Run() = %#v, %v; requests = %#v", got, err, fake.Requests)
|
t.Fatalf("Run() = %#v, %v; requests = %#v", got, err, fake.Requests)
|
||||||
}
|
}
|
||||||
|
req.References[0].Path = "/references/changed.json"
|
||||||
|
if fake.Requests[0].References[0].Path != "/references/party.json" {
|
||||||
|
t.Fatalf("fake retained aliased request references: %#v", fake.Requests[0].References)
|
||||||
|
}
|
||||||
|
|
||||||
wantErr := errors.New("configured failure")
|
wantErr := errors.New("configured failure")
|
||||||
fake.Err = wantErr
|
fake.Err = wantErr
|
||||||
|
|||||||
Reference in New Issue
Block a user