feat: complete catalog-driven preprocessing
Publish documentation / publish (push) Successful in 2m12s
Publish documentation / publish (push) Successful in 2m12s
This commit is contained in:
@@ -83,14 +83,11 @@ Commands:
|
||||
Verify a terminal installation, remove stale lifecycle files, and clear maintenance.
|
||||
pi logs Show the latest 200 sanitized core log lines (bounded; no follow mode).
|
||||
workspace inspect --workspace ID [--json]
|
||||
Inspect the active registry snapshot for one workspace.
|
||||
workspace preprocess dwh --workspace ID [--resume RUN] [--json]
|
||||
workspace schema suggest-fks --workspace ID [--from-sql FILE]... [--assume COLUMN=TABLE]... [--output FILE] [--json]
|
||||
workspace schema check --workspace ID [--annotations FILE --reviewed-candidates sha256:HEX] [--json]
|
||||
workspace schema accept --workspace ID --run RUN --yes [--json]
|
||||
workspace index-schema --workspace ID [--json]
|
||||
workspace preprocess evidence --workspace ID [--dry-run] [--resume RUN] [--json]
|
||||
workspace preprocess run --workspace ID [--resume RUN] [--json]
|
||||
Inspect the active registry snapshot for one workspace.
|
||||
workspace preprocess run --workspace ID [--json]
|
||||
Rebuild Catalog metadata, LSH, schema/Evidence vectors, then enable the core.
|
||||
workspace preprocess clear --workspace ID [--json]
|
||||
Clear schema/Evidence vectors and LSH while preserving workspace memory.
|
||||
`
|
||||
|
||||
var (
|
||||
|
||||
@@ -1255,28 +1255,20 @@ func TestRunWorkspaceInspectDispatchesThroughTheMaintenanceService(t *testing.T)
|
||||
assertInvocationContains(t, fixture.invocations(t), "workspace-maintenance", "inspect")
|
||||
}
|
||||
|
||||
func TestRunWorkspaceBlockedResultsExitThreeAndRenderHumanOutput(t *testing.T) {
|
||||
func TestRunWorkspaceRejectsRemovedSchemaCommandsBeforeDocker(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
t.Setenv("THT_FAKE_WORKSPACE_RESULT", `{"schemaVersion":1,"status":"blocked","code":"manual_review_required","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:`+strings.Repeat("b", 64)+`","operation":"schema-suggest-fks","completedStages":["dwh"],"warnings":["review required"]}`)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{"--installation", fixture.installationPath, "workspace", "schema", "suggest-fks", "--workspace", "abc"}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 3 {
|
||||
t.Fatalf("run() exit code = %d, want 3", exitCode)
|
||||
if exitCode != 2 {
|
||||
t.Fatalf("run() exit code = %d, want 2", exitCode)
|
||||
}
|
||||
for _, expected := range []string{"workspace: abc", "status: blocked", "warning: review required"} {
|
||||
if !strings.Contains(stdout.String(), expected) {
|
||||
t.Fatalf("stdout = %q, missing %q", stdout.String(), expected)
|
||||
}
|
||||
}
|
||||
if strings.Contains(stdout.String(), "descriptorBlob") || strings.Contains(stdout.String(), strings.Repeat("b", 64)) {
|
||||
t.Fatalf("stdout leaked non-allowlisted fields: %q", stdout.String())
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
if !strings.Contains(stderr.String(), `unknown workspace command "schema"`) {
|
||||
t.Fatalf("stderr = %q", stderr.String())
|
||||
}
|
||||
assertDockerNotInvoked(t, fixture)
|
||||
}
|
||||
|
||||
func TestRunWorkspaceRequiresWorkspaceFlagBeforeDocker(t *testing.T) {
|
||||
@@ -1317,26 +1309,26 @@ func TestRunWorkspaceRejectsDuplicateWorkspaceFlagsBeforeDocker(t *testing.T) {
|
||||
assertDockerNotInvoked(t, fixture)
|
||||
}
|
||||
|
||||
func TestRunWorkspaceRejectsInvalidResumeRunIDsBeforeDocker(t *testing.T) {
|
||||
func TestRunWorkspaceRejectsResumeOnOneShotPreprocessingBeforeDocker(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
exitCode := run(context.Background(), []string{
|
||||
"--installation", fixture.installationPath,
|
||||
"workspace", "preprocess", "dwh", "--workspace", "abc", "--resume", "not-a-run-id",
|
||||
"workspace", "preprocess", "run", "--workspace", "abc", "--resume", "not-a-run-id",
|
||||
}, &stdout, &stderr)
|
||||
|
||||
if exitCode != 2 {
|
||||
t.Fatalf("run() exit code = %d, want 2", exitCode)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "--resume") || !strings.Contains(stderr.String(), "32 lowercase hex") {
|
||||
if !strings.Contains(stderr.String(), "--resume") || !strings.Contains(stderr.String(), "unknown workspace option") {
|
||||
t.Fatalf("stderr = %q", stderr.String())
|
||||
}
|
||||
assertDockerNotInvoked(t, fixture)
|
||||
}
|
||||
|
||||
func TestRunWorkspaceSchemaCheckRequiresReviewedCandidatesWithAnnotations(t *testing.T) {
|
||||
func TestRunWorkspaceRejectsRemovedSchemaCheckBeforeDocker(t *testing.T) {
|
||||
fixture := newCLIFixture(t, "SAFE_VALUE=1\n")
|
||||
fixture.setEnvironment(t)
|
||||
annotations := filepath.Join(fixture.root, "annotations.yaml")
|
||||
@@ -1353,7 +1345,7 @@ func TestRunWorkspaceSchemaCheckRequiresReviewedCandidatesWithAnnotations(t *tes
|
||||
if exitCode != 2 {
|
||||
t.Fatalf("run() exit code = %d, want 2", exitCode)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "--reviewed-candidates") {
|
||||
if !strings.Contains(stderr.String(), `unknown workspace command "schema"`) {
|
||||
t.Fatalf("stderr = %q", stderr.String())
|
||||
}
|
||||
assertDockerNotInvoked(t, fixture)
|
||||
|
||||
@@ -4,7 +4,6 @@ package workspaceops
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -17,19 +16,9 @@ import (
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||
)
|
||||
|
||||
var (
|
||||
workspacePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{2,62}$`)
|
||||
runIDPattern = regexp.MustCompile(`^[0-9a-f]{32}$`)
|
||||
reviewedCandidatesDigest = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
)
|
||||
|
||||
const (
|
||||
maxFromSQLFiles = 32
|
||||
maxAssumptions = 256
|
||||
)
|
||||
var workspacePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{2,62}$`)
|
||||
|
||||
type Runner interface {
|
||||
Run(context.Context, []string, io.Reader) (compose.Result, error)
|
||||
@@ -53,136 +42,32 @@ func (b baseRequest) JSONMode() bool { return b.JSON }
|
||||
|
||||
type InspectRequest struct{ baseRequest }
|
||||
|
||||
type DwhRequest struct {
|
||||
baseRequest
|
||||
Resume string
|
||||
}
|
||||
type RunRequest struct{ baseRequest }
|
||||
type ClearRequest struct{ baseRequest }
|
||||
|
||||
type SuggestFksRequest struct {
|
||||
baseRequest
|
||||
FromSQL []string
|
||||
Assume []string
|
||||
Output string
|
||||
}
|
||||
func (InspectRequest) workspaceRequest() {}
|
||||
func (RunRequest) workspaceRequest() {}
|
||||
func (ClearRequest) workspaceRequest() {}
|
||||
|
||||
type CheckSchemaRequest struct {
|
||||
baseRequest
|
||||
Annotations string
|
||||
ReviewedCandidates string
|
||||
}
|
||||
|
||||
type AcceptSchemaRequest struct {
|
||||
baseRequest
|
||||
Run string
|
||||
Yes bool
|
||||
}
|
||||
|
||||
type IndexSchemaRequest struct{ baseRequest }
|
||||
|
||||
type EvidenceRequest struct {
|
||||
baseRequest
|
||||
DryRun bool
|
||||
Resume string
|
||||
}
|
||||
|
||||
type RunRequest struct {
|
||||
baseRequest
|
||||
Resume string
|
||||
}
|
||||
|
||||
func (InspectRequest) workspaceRequest() {}
|
||||
func (DwhRequest) workspaceRequest() {}
|
||||
func (SuggestFksRequest) workspaceRequest() {}
|
||||
func (CheckSchemaRequest) workspaceRequest() {}
|
||||
func (AcceptSchemaRequest) workspaceRequest() {}
|
||||
func (IndexSchemaRequest) workspaceRequest() {}
|
||||
func (EvidenceRequest) workspaceRequest() {}
|
||||
func (RunRequest) workspaceRequest() {}
|
||||
|
||||
func (InspectRequest) operatorCommand() string { return "inspect" }
|
||||
func (DwhRequest) operatorCommand() string { return "preprocess-dwh" }
|
||||
func (SuggestFksRequest) operatorCommand() string { return "schema-suggest-fks" }
|
||||
func (CheckSchemaRequest) operatorCommand() string {
|
||||
return "schema-check"
|
||||
}
|
||||
func (AcceptSchemaRequest) operatorCommand() string { return "schema-accept" }
|
||||
func (IndexSchemaRequest) operatorCommand() string { return "index-schema" }
|
||||
func (EvidenceRequest) operatorCommand() string { return "preprocess-evidence" }
|
||||
func (RunRequest) operatorCommand() string { return "preprocess-run" }
|
||||
func (InspectRequest) operatorCommand() string { return "inspect" }
|
||||
func (RunRequest) operatorCommand() string { return "preprocess-run" }
|
||||
func (ClearRequest) operatorCommand() string { return "preprocess-clear" }
|
||||
|
||||
func (r InspectRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace}, nil
|
||||
}
|
||||
|
||||
func (r DwhRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace, Resume: r.Resume}, nil
|
||||
}
|
||||
|
||||
func (r SuggestFksRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
envelope := requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace, Assume: append([]string(nil), r.Assume...)}
|
||||
totalBytes := 0
|
||||
for _, path := range r.FromSQL {
|
||||
contents, err := safeio.ReadCanonicalUTF8(path, 1<<20)
|
||||
if err != nil {
|
||||
return requestEnvelope{}, errors.New("SQL input could not be read safely")
|
||||
}
|
||||
totalBytes += len(contents)
|
||||
if totalBytes > 16<<20 {
|
||||
return requestEnvelope{}, errors.New("SQL input total exceeds 16 MiB")
|
||||
}
|
||||
envelope.SQLFiles = append(envelope.SQLFiles, inputFile{Name: filepath.Base(path), SQL: contents})
|
||||
}
|
||||
return envelope, nil
|
||||
}
|
||||
|
||||
func (r CheckSchemaRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
annotations, err := safeio.ReadCanonicalUTF8(r.Annotations, 16<<20)
|
||||
if err != nil {
|
||||
return requestEnvelope{}, errors.New("annotation file could not be read safely")
|
||||
}
|
||||
return requestEnvelope{
|
||||
SchemaVersion: 1,
|
||||
WorkspaceID: r.Workspace,
|
||||
Annotations: annotations,
|
||||
ReviewedCandidates: r.ReviewedCandidates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r IndexSchemaRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
func (r RunRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace}, nil
|
||||
}
|
||||
|
||||
func (r AcceptSchemaRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace, RunID: r.Run, Yes: r.Yes}, nil
|
||||
}
|
||||
|
||||
func (r EvidenceRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace, Resume: r.Resume, DryRun: r.DryRun}, nil
|
||||
}
|
||||
|
||||
func (r RunRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace, Resume: r.Resume}, nil
|
||||
func (r ClearRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace}, nil
|
||||
}
|
||||
|
||||
type requestEnvelope struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
Resume string `json:"resumeRunId,omitempty"`
|
||||
DryRun bool `json:"dryRun,omitempty"`
|
||||
Assume []string `json:"assume,omitempty"`
|
||||
SQLFiles []inputFile `json:"fromSql,omitempty"`
|
||||
Annotations string `json:"annotationsYaml,omitempty"`
|
||||
ReviewedCandidates string `json:"reviewedCandidatesDigest,omitempty"`
|
||||
Collection string `json:"collection,omitempty"`
|
||||
Confirm string `json:"confirm,omitempty"`
|
||||
Destroy bool `json:"destroy,omitempty"`
|
||||
RunID string `json:"runId,omitempty"`
|
||||
Yes bool `json:"yes,omitempty"`
|
||||
}
|
||||
|
||||
type inputFile struct {
|
||||
Name string `json:"name"`
|
||||
SQL string `json:"sql"`
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -198,7 +83,6 @@ type Result struct {
|
||||
CompletedStages []string `json:"completedStages"`
|
||||
Counts map[string]int `json:"counts,omitempty"`
|
||||
ArtifactIdentities []ArtifactIdentity `json:"artifactIdentities,omitempty"`
|
||||
SuggestedFksYAML string `json:"suggestedFksYaml,omitempty"`
|
||||
EffectiveConfigIdentity string `json:"effectiveConfigIdentity,omitempty"`
|
||||
ConfigFingerprint string `json:"configFingerprint,omitempty"`
|
||||
InputFingerprint string `json:"inputFingerprint,omitempty"`
|
||||
@@ -212,7 +96,6 @@ type ArtifactIdentity struct {
|
||||
|
||||
type operationResponse struct {
|
||||
Result
|
||||
SuggestedFksYAML string `json:"suggestedFksYaml,omitempty"`
|
||||
}
|
||||
|
||||
type Stage string
|
||||
@@ -266,35 +149,11 @@ func Parse(args []string) (Request, error) {
|
||||
return parsed, nil
|
||||
case "preprocess":
|
||||
return parsePreprocess(args[1:])
|
||||
case "schema":
|
||||
return parseSchema(args[1:])
|
||||
case "index-schema":
|
||||
parsed, err := parseIndexSchema(args[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsed, nil
|
||||
case "vector":
|
||||
return parseVector(args[1:])
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown workspace command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func parseVector(args []string) (Request, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("vector requires a subcommand")
|
||||
}
|
||||
switch args[0] {
|
||||
case "inspect":
|
||||
return parseVectorInspect(args[1:])
|
||||
case "rebuild":
|
||||
return parseVectorRebuild(args[1:])
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown vector command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func Execute(ctx context.Context, installation config.Installation, runner Runner, request Request) (Result, error) {
|
||||
envelope, err := request.stdinEnvelope()
|
||||
if err != nil {
|
||||
@@ -340,21 +199,6 @@ func Execute(ctx context.Context, installation config.Installation, runner Runne
|
||||
}
|
||||
return Result{}, parseErr
|
||||
}
|
||||
if suggest, ok := request.(SuggestFksRequest); ok && suggest.Output != "" {
|
||||
if response.SuggestedFksYAML == "" {
|
||||
return Result{}, errors.New("workspace maintenance did not return the requested FK artifact")
|
||||
}
|
||||
if digest := suggestedArtifactDigest(response); digest != "" {
|
||||
sum := sha256.Sum256([]byte(response.SuggestedFksYAML))
|
||||
if digest != "sha256:"+fmt.Sprintf("%x", sum[:]) {
|
||||
return Result{}, errors.New("workspace maintenance returned an FK artifact with a mismatched digest")
|
||||
}
|
||||
}
|
||||
if err := safeio.WriteCanonicalNewFile(suggest.Output, []byte(response.SuggestedFksYAML), 0o600); err != nil {
|
||||
return Result{}, errors.New("workspace FK output file could not be created safely")
|
||||
}
|
||||
}
|
||||
response.Result.SuggestedFksYAML = response.SuggestedFksYAML
|
||||
return response.Result, nil
|
||||
}
|
||||
|
||||
@@ -428,7 +272,7 @@ func isLowerHex(value string) bool {
|
||||
}
|
||||
|
||||
func parseInspect(args []string) (InspectRequest, error) {
|
||||
base, err := parseBaseFlags(args, false)
|
||||
base, err := parseBaseFlags(args)
|
||||
if err != nil {
|
||||
return InspectRequest{}, err
|
||||
}
|
||||
@@ -437,299 +281,61 @@ func parseInspect(args []string) (InspectRequest, error) {
|
||||
|
||||
func parsePreprocess(args []string) (Request, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("workspace preprocess requires dwh, evidence, or run")
|
||||
return nil, errors.New("workspace preprocess requires run or clear")
|
||||
}
|
||||
switch args[0] {
|
||||
case "dwh":
|
||||
base, resume, dryRun, err := parseResumeFlags(args[1:], false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dryRun {
|
||||
return nil, errors.New("workspace preprocess dwh does not accept --dry-run")
|
||||
}
|
||||
return DwhRequest{baseRequest: base, Resume: resume}, nil
|
||||
case "evidence":
|
||||
base, resume, dryRun, err := parseResumeFlags(args[1:], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return EvidenceRequest{baseRequest: base, Resume: resume, DryRun: dryRun}, nil
|
||||
case "run":
|
||||
base, resume, dryRun, err := parseResumeFlags(args[1:], false)
|
||||
base, err := parseBaseFlags(args[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dryRun {
|
||||
return nil, errors.New("workspace preprocess run does not accept --dry-run")
|
||||
return RunRequest{baseRequest: base}, nil
|
||||
case "clear":
|
||||
base, err := parseBaseFlags(args[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return RunRequest{baseRequest: base, Resume: resume}, nil
|
||||
return ClearRequest{baseRequest: base}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown workspace preprocess command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func parseSchema(args []string) (Request, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("workspace schema requires suggest-fks or check")
|
||||
}
|
||||
switch args[0] {
|
||||
case "suggest-fks":
|
||||
return parseSuggestFks(args[1:])
|
||||
case "check":
|
||||
return parseSchemaCheck(args[1:])
|
||||
case "accept":
|
||||
return parseSchemaAccept(args[1:])
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown workspace schema command %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func parseIndexSchema(args []string) (IndexSchemaRequest, error) {
|
||||
base, err := parseBaseFlags(args, false)
|
||||
if err != nil {
|
||||
return IndexSchemaRequest{}, err
|
||||
}
|
||||
return IndexSchemaRequest{baseRequest: base}, nil
|
||||
}
|
||||
|
||||
func parseResumeFlags(args []string, allowDryRun bool) (baseRequest, string, bool, error) {
|
||||
var resume string
|
||||
var dryRun bool
|
||||
base, seen, err := parseSharedFlags(args, map[string]func(string) error{
|
||||
"--resume": func(value string) error {
|
||||
if resume != "" {
|
||||
return errors.New("--resume may be supplied once")
|
||||
}
|
||||
if !runIDPattern.MatchString(value) {
|
||||
return errors.New("--resume must be 32 lowercase hex characters")
|
||||
}
|
||||
resume = value
|
||||
return nil
|
||||
},
|
||||
}, map[string]func() error{
|
||||
"--dry-run": func() error {
|
||||
if !allowDryRun {
|
||||
return errors.New("--dry-run is not accepted here")
|
||||
}
|
||||
if dryRun {
|
||||
return errors.New("--dry-run may be supplied once")
|
||||
}
|
||||
dryRun = true
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return baseRequest{}, "", false, err
|
||||
}
|
||||
if !seen.workspace {
|
||||
return baseRequest{}, "", false, errors.New("--workspace is required")
|
||||
}
|
||||
return base, resume, dryRun, nil
|
||||
}
|
||||
|
||||
func parseSuggestFks(args []string) (SuggestFksRequest, error) {
|
||||
request := SuggestFksRequest{}
|
||||
base, seen, err := parseSharedFlags(args, map[string]func(string) error{
|
||||
"--from-sql": func(value string) error {
|
||||
if len(request.FromSQL) >= maxFromSQLFiles {
|
||||
return fmt.Errorf("--from-sql may be supplied at most %d times", maxFromSQLFiles)
|
||||
}
|
||||
request.FromSQL = append(request.FromSQL, value)
|
||||
return nil
|
||||
},
|
||||
"--assume": func(value string) error {
|
||||
if len(request.Assume) >= maxAssumptions {
|
||||
return fmt.Errorf("--assume may be supplied at most %d times", maxAssumptions)
|
||||
}
|
||||
if len(value) > 256 || !strings.Contains(value, "=") {
|
||||
return errors.New("--assume values must be column=table entries up to 256 bytes")
|
||||
}
|
||||
left, right, _ := strings.Cut(value, "=")
|
||||
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
||||
return errors.New("--assume values must be column=table entries up to 256 bytes")
|
||||
}
|
||||
request.Assume = append(request.Assume, value)
|
||||
return nil
|
||||
},
|
||||
"--output": func(value string) error {
|
||||
if request.Output != "" {
|
||||
return errors.New("--output may be supplied once")
|
||||
}
|
||||
request.Output = value
|
||||
return nil
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return SuggestFksRequest{}, err
|
||||
}
|
||||
if !seen.workspace {
|
||||
return SuggestFksRequest{}, errors.New("--workspace is required")
|
||||
}
|
||||
request.baseRequest = base
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func parseSchemaCheck(args []string) (CheckSchemaRequest, error) {
|
||||
request := CheckSchemaRequest{}
|
||||
base, seen, err := parseSharedFlags(args, map[string]func(string) error{
|
||||
"--annotations": func(value string) error {
|
||||
if request.Annotations != "" {
|
||||
return errors.New("--annotations may be supplied once")
|
||||
}
|
||||
request.Annotations = value
|
||||
return nil
|
||||
},
|
||||
"--reviewed-candidates": func(value string) error {
|
||||
if request.ReviewedCandidates != "" {
|
||||
return errors.New("--reviewed-candidates may be supplied once")
|
||||
}
|
||||
if !reviewedCandidatesDigest.MatchString(value) {
|
||||
return errors.New("--reviewed-candidates must be sha256:<64 lowercase hex>")
|
||||
}
|
||||
request.ReviewedCandidates = value
|
||||
return nil
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return CheckSchemaRequest{}, err
|
||||
}
|
||||
if !seen.workspace {
|
||||
return CheckSchemaRequest{}, errors.New("--workspace is required")
|
||||
}
|
||||
if (request.Annotations == "") != (request.ReviewedCandidates == "") {
|
||||
return CheckSchemaRequest{}, errors.New("--annotations and --reviewed-candidates must be supplied together")
|
||||
}
|
||||
request.baseRequest = base
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func parseSchemaAccept(args []string) (AcceptSchemaRequest, error) {
|
||||
request := AcceptSchemaRequest{}
|
||||
base, seen, err := parseSharedFlags(args, map[string]func(string) error{
|
||||
"--run": func(value string) error {
|
||||
if request.Run != "" {
|
||||
return errors.New("--run may be supplied once")
|
||||
}
|
||||
if !runIDPattern.MatchString(value) {
|
||||
return errors.New("--run must be 32 lowercase hex characters")
|
||||
}
|
||||
request.Run = value
|
||||
return nil
|
||||
},
|
||||
}, map[string]func() error{
|
||||
"--yes": func() error {
|
||||
if request.Yes {
|
||||
return errors.New("--yes may be supplied once")
|
||||
}
|
||||
request.Yes = true
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return AcceptSchemaRequest{}, err
|
||||
}
|
||||
if !seen.workspace {
|
||||
return AcceptSchemaRequest{}, errors.New("--workspace is required")
|
||||
}
|
||||
if request.Run == "" {
|
||||
return AcceptSchemaRequest{}, errors.New("--run is required")
|
||||
}
|
||||
if !request.Yes {
|
||||
return AcceptSchemaRequest{}, errors.New("--yes is required")
|
||||
}
|
||||
request.baseRequest = base
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func parseBaseFlags(args []string, allowDryRun bool) (baseRequest, error) {
|
||||
base, seen, err := parseSharedFlags(args, nil, nil)
|
||||
if err != nil {
|
||||
return baseRequest{}, err
|
||||
}
|
||||
if !seen.workspace {
|
||||
return baseRequest{}, errors.New("--workspace is required")
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
||||
type seenFlags struct {
|
||||
workspace bool
|
||||
json bool
|
||||
}
|
||||
|
||||
func parseSharedFlags(args []string, valueHandlers map[string]func(string) error, boolHandlers map[string]func() error) (baseRequest, seenFlags, error) {
|
||||
func parseBaseFlags(args []string) (baseRequest, error) {
|
||||
request := baseRequest{}
|
||||
seen := seenFlags{}
|
||||
valueHandlers = cloneValueHandlers(valueHandlers)
|
||||
boolHandlers = cloneBoolHandlers(boolHandlers)
|
||||
workspaceSeen := false
|
||||
jsonSeen := false
|
||||
for len(args) > 0 {
|
||||
flag := args[0]
|
||||
if flag == "--" {
|
||||
return baseRequest{}, seenFlags{}, errors.New("passthrough separators are not supported")
|
||||
return baseRequest{}, errors.New("passthrough separators are not supported")
|
||||
}
|
||||
switch flag {
|
||||
case "--workspace":
|
||||
if len(args) < 2 {
|
||||
return baseRequest{}, seenFlags{}, errors.New("--workspace requires a value")
|
||||
return baseRequest{}, errors.New("--workspace requires a value")
|
||||
}
|
||||
if seen.workspace {
|
||||
return baseRequest{}, seenFlags{}, errors.New("--workspace must be supplied exactly once")
|
||||
if workspaceSeen {
|
||||
return baseRequest{}, errors.New("--workspace must be supplied exactly once")
|
||||
}
|
||||
workspace := args[1]
|
||||
if !workspacePattern.MatchString(workspace) {
|
||||
return baseRequest{}, seenFlags{}, errors.New("--workspace must match [a-z][a-z0-9-]{2,62}")
|
||||
return baseRequest{}, errors.New("--workspace must match [a-z][a-z0-9-]{2,62}")
|
||||
}
|
||||
request.Workspace, seen.workspace, args = workspace, true, args[2:]
|
||||
request.Workspace, workspaceSeen, args = workspace, true, args[2:]
|
||||
case "--json":
|
||||
if seen.json {
|
||||
return baseRequest{}, seenFlags{}, errors.New("--json may be supplied once")
|
||||
if jsonSeen {
|
||||
return baseRequest{}, errors.New("--json may be supplied once")
|
||||
}
|
||||
request.JSON, seen.json, args = true, true, args[1:]
|
||||
request.JSON, jsonSeen, args = true, true, args[1:]
|
||||
default:
|
||||
if handler, ok := boolHandlers[flag]; ok {
|
||||
if err := handler(); err != nil {
|
||||
return baseRequest{}, seenFlags{}, err
|
||||
}
|
||||
args = args[1:]
|
||||
continue
|
||||
}
|
||||
handler, ok := valueHandlers[flag]
|
||||
if !ok {
|
||||
return baseRequest{}, seenFlags{}, fmt.Errorf("unknown workspace option %q", flag)
|
||||
}
|
||||
if len(args) < 2 {
|
||||
return baseRequest{}, seenFlags{}, fmt.Errorf("%s requires a value", flag)
|
||||
}
|
||||
if err := handler(args[1]); err != nil {
|
||||
return baseRequest{}, seenFlags{}, err
|
||||
}
|
||||
args = args[2:]
|
||||
return baseRequest{}, fmt.Errorf("unknown workspace option %q", flag)
|
||||
}
|
||||
}
|
||||
return request, seen, nil
|
||||
}
|
||||
|
||||
func cloneValueHandlers(source map[string]func(string) error) map[string]func(string) error {
|
||||
if len(source) == 0 {
|
||||
return map[string]func(string) error{}
|
||||
if !workspaceSeen {
|
||||
return baseRequest{}, errors.New("--workspace is required")
|
||||
}
|
||||
clone := make(map[string]func(string) error, len(source))
|
||||
for key, handler := range source {
|
||||
clone[key] = handler
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
func cloneBoolHandlers(source map[string]func() error) map[string]func() error {
|
||||
if len(source) == 0 {
|
||||
return map[string]func() error{}
|
||||
}
|
||||
clone := make(map[string]func() error, len(source))
|
||||
for key, handler := range source {
|
||||
clone[key] = handler
|
||||
}
|
||||
return clone
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func selectedCoreImage(document string) (string, error) {
|
||||
@@ -841,15 +447,6 @@ func runDocker(ctx context.Context, runner Runner, stage Stage, args []string, s
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func suggestedArtifactDigest(response operationResponse) string {
|
||||
for _, artifact := range response.ArtifactIdentities {
|
||||
if strings.HasPrefix(artifact.Digest, "sha256:") {
|
||||
return artifact.Digest
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func Human(result Result) string {
|
||||
lines := []string{
|
||||
fmt.Sprintf("workspace: %s", result.WorkspaceID),
|
||||
@@ -871,71 +468,3 @@ func Human(result Result) string {
|
||||
}
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
// VectorInspectRequest reads the Qdrant collection contract without mutation.
|
||||
type VectorInspectRequest struct{ baseRequest }
|
||||
|
||||
// VectorRebuildRequest deletes and recreates the descriptor-owned collection under guards.
|
||||
type VectorRebuildRequest struct {
|
||||
baseRequest
|
||||
Collection string
|
||||
Confirm string
|
||||
Destroy bool
|
||||
}
|
||||
|
||||
func (VectorInspectRequest) workspaceRequest() {}
|
||||
func (VectorRebuildRequest) workspaceRequest() {}
|
||||
func (VectorInspectRequest) operatorCommand() string { return "vector-inspect" }
|
||||
func (VectorRebuildRequest) operatorCommand() string { return "vector-rebuild" }
|
||||
func (r VectorInspectRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
return requestEnvelope{SchemaVersion: 1, WorkspaceID: r.Workspace}, nil
|
||||
}
|
||||
func (r VectorRebuildRequest) stdinEnvelope() (requestEnvelope, error) {
|
||||
if r.Collection == "" {
|
||||
return requestEnvelope{}, errors.New("--collection is required")
|
||||
}
|
||||
if r.Confirm == "" {
|
||||
return requestEnvelope{}, errors.New("--confirm is required and must equal --collection")
|
||||
}
|
||||
if r.Confirm != r.Collection {
|
||||
return requestEnvelope{}, errors.New("--confirm must equal --collection")
|
||||
}
|
||||
if !r.Destroy {
|
||||
return requestEnvelope{}, errors.New("--destroy is required to confirm the destructive rebuild")
|
||||
}
|
||||
return requestEnvelope{
|
||||
SchemaVersion: 1,
|
||||
WorkspaceID: r.Workspace,
|
||||
Collection: r.Collection,
|
||||
Confirm: r.Confirm,
|
||||
Destroy: r.Destroy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseVectorInspect(args []string) (Request, error) {
|
||||
base, err := parseBaseFlags(args, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return VectorInspectRequest{baseRequest: base}, nil
|
||||
}
|
||||
|
||||
func parseVectorRebuild(args []string) (Request, error) {
|
||||
request := VectorRebuildRequest{}
|
||||
values := map[string]func(string) error{
|
||||
"--collection": func(v string) error { request.Collection = v; return nil },
|
||||
"--confirm": func(v string) error { request.Confirm = v; return nil },
|
||||
}
|
||||
bools := map[string]func() error{
|
||||
"--destroy": func() error { request.Destroy = true; return nil },
|
||||
}
|
||||
base, seen, err := parseSharedFlags(args, values, bools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !seen.workspace {
|
||||
return nil, errors.New("--workspace is required")
|
||||
}
|
||||
request.baseRequest = base
|
||||
return request, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package workspaceops
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -25,139 +23,84 @@ type fakeRunner struct {
|
||||
func (r *fakeRunner) Run(_ context.Context, args []string, stdin io.Reader) (compose.Result, error) {
|
||||
payload := ""
|
||||
if stdin != nil {
|
||||
bytes, err := io.ReadAll(stdin)
|
||||
contents, err := io.ReadAll(stdin)
|
||||
if err != nil {
|
||||
return compose.Result{}, err
|
||||
}
|
||||
payload = string(bytes)
|
||||
payload = string(contents)
|
||||
}
|
||||
r.all = append(r.all, append([]string(nil), args...))
|
||||
r.stdins = append(r.stdins, payload)
|
||||
return r.run(args, payload)
|
||||
}
|
||||
|
||||
func TestExecuteSuggestFksStreamsSQLFileContentsOnStdin(t *testing.T) {
|
||||
func TestExecuteRunSendsOnlyTheOneShotEnvelope(t *testing.T) {
|
||||
installation := testInstallation(t)
|
||||
sqlPath := filepath.Join(filepath.Dir(installation.Path), "query.sql")
|
||||
if err := os.WriteFile(sqlPath, []byte("select 1;\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runner := &fakeRunner{run: func(args []string, stdin string) (compose.Result, error) {
|
||||
switch {
|
||||
case contains(args, "config", "--format", "json"):
|
||||
return compose.Result{Stdout: `{"services":{"core":{"image":"thothii-core:local"}}}`}, nil
|
||||
case reflect.DeepEqual(args, []string{"image", "inspect", "--format", "{{.Id}}", "thothii-core:local"}):
|
||||
return compose.Result{Stdout: "sha256:" + strings.Repeat("a", 64)}, nil
|
||||
case contains(args, "workspace-maintenance", "schema-suggest-fks"):
|
||||
case contains(args, "workspace-maintenance", "preprocess-run"):
|
||||
var envelope map[string]any
|
||||
if err := json.Unmarshal([]byte(stdin), &envelope); err != nil {
|
||||
t.Fatalf("stdin JSON = %q, err=%v", stdin, err)
|
||||
}
|
||||
sqlFiles, ok := envelope["fromSql"].([]any)
|
||||
if !ok || len(sqlFiles) != 1 {
|
||||
t.Fatalf("sqlFiles = %#v", envelope["fromSql"])
|
||||
if !reflect.DeepEqual(envelope, map[string]any{
|
||||
"schemaVersion": float64(1),
|
||||
"workspaceId": "abc",
|
||||
}) {
|
||||
t.Fatalf("unexpected envelope: %#v", envelope)
|
||||
}
|
||||
file, ok := sqlFiles[0].(map[string]any)
|
||||
if !ok || file["sql"] != "select 1;\n" || file["name"] != "query.sql" {
|
||||
t.Fatalf("sql file envelope = %#v", sqlFiles[0])
|
||||
}
|
||||
return compose.Result{Stdout: successResult("schema-suggest-fks")}, nil
|
||||
return compose.Result{Stdout: successResult("preprocess run")}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected Docker invocation: %#v", args)
|
||||
return compose.Result{}, nil
|
||||
}
|
||||
}}
|
||||
|
||||
_, err := Execute(context.Background(), installation, runner, SuggestFksRequest{baseRequest: baseRequest{Workspace: "abc"}, FromSQL: []string{sqlPath}})
|
||||
result, err := Execute(
|
||||
context.Background(),
|
||||
installation,
|
||||
runner,
|
||||
RunRequest{baseRequest: baseRequest{Workspace: "abc", JSON: true}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSuggestFksRejectsTotalSQLIngressOverSixteenMiB(t *testing.T) {
|
||||
installation := testInstallation(t)
|
||||
paths := make([]string, 0, 17)
|
||||
for index := 0; index < 17; index++ {
|
||||
path := filepath.Join(filepath.Dir(installation.Path), fmt.Sprintf("query-%02d.sql", index))
|
||||
if err := os.WriteFile(path, bytes.Repeat([]byte("x"), 1<<20), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
paths = append(paths, path)
|
||||
}
|
||||
runner := &fakeRunner{run: func(args []string, stdin string) (compose.Result, error) {
|
||||
t.Fatalf("Docker should not run when total SQL ingress exceeds the bound: %#v", args)
|
||||
return compose.Result{}, nil
|
||||
}}
|
||||
|
||||
_, err := Execute(context.Background(), installation, runner, SuggestFksRequest{baseRequest: baseRequest{Workspace: "abc"}, FromSQL: paths})
|
||||
if err == nil || !strings.Contains(err.Error(), "total") {
|
||||
t.Fatalf("Execute() error = %v, want total-size failure", err)
|
||||
if result.Status != "succeeded" || result.Operation != "preprocess run" {
|
||||
t.Fatalf("unexpected result: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSchemaCheckStreamsAnnotationContentOnStdin(t *testing.T) {
|
||||
installation := testInstallation(t)
|
||||
annotationsPath := filepath.Join(filepath.Dir(installation.Path), "annotations.yaml")
|
||||
if err := os.WriteFile(annotationsPath, []byte("reviewed: []\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runner := &fakeRunner{run: func(args []string, stdin string) (compose.Result, error) {
|
||||
switch {
|
||||
case contains(args, "config", "--format", "json"):
|
||||
return compose.Result{Stdout: `{"services":{"core":{"image":"thothii-core:local"}}}`}, nil
|
||||
case reflect.DeepEqual(args, []string{"image", "inspect", "--format", "{{.Id}}", "thothii-core:local"}):
|
||||
return compose.Result{Stdout: "sha256:" + strings.Repeat("b", 64)}, nil
|
||||
case contains(args, "workspace-maintenance", "schema-check"):
|
||||
var envelope map[string]any
|
||||
if err := json.Unmarshal([]byte(stdin), &envelope); err != nil {
|
||||
t.Fatalf("stdin JSON = %q, err=%v", stdin, err)
|
||||
}
|
||||
if envelope["annotationsYaml"] != "reviewed: []\n" || envelope["reviewedCandidatesDigest"] != "sha256:"+strings.Repeat("c", 64) {
|
||||
t.Fatalf("annotation envelope = %#v", envelope)
|
||||
}
|
||||
if _, exists := envelope["annotationsPath"]; exists {
|
||||
t.Fatalf("annotation path leaked into stdin: %#v", envelope)
|
||||
}
|
||||
return compose.Result{Stdout: successResult("schema-check")}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected Docker invocation: %#v", args)
|
||||
return compose.Result{}, nil
|
||||
func TestParseAcceptsOnlyInspectAndCompletePreprocessing(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"inspect", "--workspace", "abc", "--json"},
|
||||
{"preprocess", "run", "--workspace", "abc", "--json"},
|
||||
{"preprocess", "clear", "--workspace", "abc", "--json"},
|
||||
} {
|
||||
if _, err := Parse(args); err != nil {
|
||||
t.Fatalf("Parse(%v) error = %v", args, err)
|
||||
}
|
||||
}}
|
||||
|
||||
_, err := Execute(context.Background(), installation, runner, CheckSchemaRequest{baseRequest: baseRequest{Workspace: "abc"}, Annotations: annotationsPath, ReviewedCandidates: "sha256:" + strings.Repeat("c", 64)})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSuggestFksWritesTheReturnedCandidateArtifact(t *testing.T) {
|
||||
installation := testInstallation(t)
|
||||
outputPath := filepath.Join(filepath.Dir(installation.Path), "candidates.yaml")
|
||||
runner := &fakeRunner{run: func(args []string, stdin string) (compose.Result, error) {
|
||||
switch {
|
||||
case contains(args, "config", "--format", "json"):
|
||||
return compose.Result{Stdout: `{"services":{"core":{"image":"thothii-core:local"}}}`}, nil
|
||||
case reflect.DeepEqual(args, []string{"image", "inspect", "--format", "{{.Id}}", "thothii-core:local"}):
|
||||
return compose.Result{Stdout: "sha256:" + strings.Repeat("d", 64)}, nil
|
||||
case contains(args, "workspace-maintenance", "schema-suggest-fks"):
|
||||
return compose.Result{Stdout: `{"schemaVersion":1,"status":"blocked","code":"manual_review_required","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:` + strings.Repeat("e", 64) + `","operation":"schema-suggest-fks","completedStages":[],"suggestedFksYaml":"reviewed: []\n"}`}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected Docker invocation: %#v", args)
|
||||
return compose.Result{}, nil
|
||||
retired := [][]string{
|
||||
{"preprocess", "dwh", "--workspace", "abc"},
|
||||
{"preprocess", "evidence", "--workspace", "abc"},
|
||||
{"preprocess", "run", "--workspace", "abc", "--resume", strings.Repeat("a", 32)},
|
||||
{"preprocess", "run", "--workspace", "abc", "--dry-run"},
|
||||
{"schema", "suggest-fks", "--workspace", "abc"},
|
||||
{"schema", "check", "--workspace", "abc"},
|
||||
{"schema", "accept", "--workspace", "abc"},
|
||||
{"index-schema", "--workspace", "abc"},
|
||||
{"vector", "inspect", "--workspace", "abc"},
|
||||
{"vector", "rebuild", "--workspace", "abc"},
|
||||
}
|
||||
for _, args := range retired {
|
||||
if _, err := Parse(args); err == nil {
|
||||
t.Fatalf("retired command accepted: %v", args)
|
||||
}
|
||||
}}
|
||||
|
||||
_, err := Execute(context.Background(), installation, runner, SuggestFksRequest{baseRequest: baseRequest{Workspace: "abc"}, Output: outputPath})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
contents, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("output artifact missing: %v", err)
|
||||
}
|
||||
if string(contents) != "reviewed: []\n" {
|
||||
t.Fatalf("output contents = %q", contents)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +129,10 @@ func testInstallation(t *testing.T) config.Installation {
|
||||
if err := os.WriteFile(envFile, []byte("SAFE_VALUE=1\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return config.Installation{Path: filepath.Join(root, "thothii-installation.yaml"), Profile: "local", ProjectDirectory: project, EnvFile: envFile}
|
||||
return config.Installation{
|
||||
Path: filepath.Join(root, "thothii-installation.yaml"), Profile: "local",
|
||||
ProjectDirectory: project, EnvFile: envFile,
|
||||
}
|
||||
}
|
||||
|
||||
func contains(values []string, sequence ...string) bool {
|
||||
@@ -201,105 +147,3 @@ func contains(values []string, sequence ...string) bool {
|
||||
func successResult(operation string) string {
|
||||
return `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:` + strings.Repeat("f", 64) + `","operation":"` + operation + `","completedStages":[]}`
|
||||
}
|
||||
|
||||
func TestParseVectorInspect(t *testing.T) {
|
||||
req, err := Parse([]string{"vector", "inspect", "--workspace", "psd", "--json"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
r, ok := req.(VectorInspectRequest)
|
||||
if !ok {
|
||||
t.Fatalf("got %T", req)
|
||||
}
|
||||
if r.Workspace != "psd" || !r.JSON {
|
||||
t.Fatalf("unexpected request: %+v", r)
|
||||
}
|
||||
env, err := req.stdinEnvelope()
|
||||
if err != nil {
|
||||
t.Fatalf("envelope: %v", err)
|
||||
}
|
||||
if env.WorkspaceID != "psd" || env.Collection != "" {
|
||||
t.Fatalf("unexpected envelope: %+v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorRebuildGuards(t *testing.T) {
|
||||
req, err := Parse([]string{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd", "--destroy"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
_, ok := req.(VectorRebuildRequest)
|
||||
if !ok {
|
||||
t.Fatalf("got %T", req)
|
||||
}
|
||||
env, err := req.stdinEnvelope()
|
||||
if err != nil {
|
||||
t.Fatalf("envelope: %v", err)
|
||||
}
|
||||
if env.Collection != "psd" || !env.Destroy {
|
||||
t.Fatalf("unexpected envelope: %+v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorRebuildRefusesMismatchedConfirmation(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "other"},
|
||||
{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd"},
|
||||
{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd", "--destroy"},
|
||||
} {
|
||||
if _, err := Parse(args); err == nil && len(args) < 7 {
|
||||
t.Fatalf("expected error for %v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorRebuildRequiresDestroy(t *testing.T) {
|
||||
req, err := Parse([]string{"vector", "rebuild", "--workspace", "psd", "--collection", "psd", "--confirm", "psd"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if _, err := req.stdinEnvelope(); err == nil {
|
||||
t.Fatal("expected envelope error without --destroy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVectorUnknownSubcommand(t *testing.T) {
|
||||
if _, err := Parse([]string{"vector", "drop", "--workspace", "psd"}); err == nil {
|
||||
t.Fatal("expected error for unknown vector command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSchemaAccept(t *testing.T) {
|
||||
req, err := Parse([]string{"schema", "accept", "--workspace", "psd", "--run", strings.Repeat("d", 32), "--yes"})
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
r, ok := req.(AcceptSchemaRequest)
|
||||
if !ok {
|
||||
t.Fatalf("got %T", req)
|
||||
}
|
||||
if r.Workspace != "psd" || r.Run != strings.Repeat("d", 32) || !r.Yes {
|
||||
t.Fatalf("unexpected request: %+v", r)
|
||||
}
|
||||
env, err := req.stdinEnvelope()
|
||||
if err != nil {
|
||||
t.Fatalf("envelope: %v", err)
|
||||
}
|
||||
if env.WorkspaceID != "psd" || env.RunID != strings.Repeat("d", 32) || !env.Yes {
|
||||
t.Fatalf("unexpected envelope: %+v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSchemaAcceptRequiresRunAndYes(t *testing.T) {
|
||||
for _, args := range [][]string{
|
||||
{"schema", "accept", "--workspace", "psd"},
|
||||
{"schema", "accept", "--workspace", "psd", "--yes"},
|
||||
{"schema", "accept", "--workspace", "psd", "--run", strings.Repeat("d", 32)},
|
||||
{"schema", "accept", "--workspace", "psd", "--run", "not-hex", "--yes"},
|
||||
{"schema", "accept", "--workspace", "psd", "--run", strings.Repeat("d", 32), "--yes", "--run", strings.Repeat("e", 32)},
|
||||
} {
|
||||
if _, err := Parse(args); err == nil {
|
||||
t.Fatalf("expected error for %v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//go:build windows
|
||||
|
||||
package workspaceops
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
)
|
||||
|
||||
func TestExecuteSuggestFksWritesOutputInInheritedOperatorDirectory(t *testing.T) {
|
||||
installation := testInstallation(t)
|
||||
operatorDirectory := filepath.Join(filepath.Dir(installation.Path), "operator-output")
|
||||
if err := os.Mkdir(operatorDirectory, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output := filepath.Join(operatorDirectory, "candidates.yaml")
|
||||
runner := &fakeRunner{run: func(args []string, _ string) (compose.Result, error) {
|
||||
switch {
|
||||
case contains(args, "config", "--format", "json"):
|
||||
return compose.Result{Stdout: `{"services":{"core":{"image":"thothii-core:local"}}}`}, nil
|
||||
case reflect.DeepEqual(args, []string{"image", "inspect", "--format", "{{.Id}}", "thothii-core:local"}):
|
||||
return compose.Result{Stdout: "sha256:" + strings.Repeat("d", 64)}, nil
|
||||
case contains(args, "workspace-maintenance", "schema-suggest-fks"):
|
||||
return compose.Result{Stdout: `{"schemaVersion":1,"status":"blocked","code":"manual_review_required","workspaceId":"abc","workspaceRevision":"1234567890abcdef1234567890abcdef12345678","descriptorBlob":"sha256:` + strings.Repeat("e", 64) + `","operation":"schema-suggest-fks","completedStages":[],"suggestedFksYaml":"reviewed: []\n"}`}, nil
|
||||
default:
|
||||
t.Fatalf("unexpected Docker invocation: %#v", args)
|
||||
return compose.Result{}, nil
|
||||
}
|
||||
}}
|
||||
|
||||
if _, err := Execute(context.Background(), installation, runner, SuggestFksRequest{baseRequest: baseRequest{Workspace: "abc"}, Output: output}); err != nil {
|
||||
t.Fatalf("workspace schema suggest-fks --output error = %v", err)
|
||||
}
|
||||
contents, err := os.ReadFile(output)
|
||||
if err != nil || string(contents) != "reviewed: []\n" {
|
||||
t.Fatalf("workspace schema suggest-fks output = %q error = %v", contents, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user