fix: harden workspace preprocessing contract

This commit is contained in:
2026-08-11 00:59:15 +02:00
parent 9df7c38069
commit 05a6e8cc2d
11 changed files with 494 additions and 32 deletions
@@ -1,7 +1,9 @@
package workspaceops
import (
"bytes"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"reflect"
@@ -132,3 +134,164 @@ func TestRunPublishesOnlyVerifiedCandidateExport(t *testing.T) {
t.Fatalf("candidate = %q, %v", b, err)
}
}
func validWorkspaceResult(status, code string) Result {
return Result{SchemaVersion: 1, Status: status, Code: code, WorkspaceID: "psd", WorkspaceRevision: strings.Repeat("0", 40), DescriptorBlob: strings.Repeat("a", 40), Operation: "evidence", CompletedStages: []string{}}
}
func TestValidateIngressRejectsExplicitOmittedEvidenceZeroFields(t *testing.T) {
command := EvidenceRequest{WorkspaceID: "psd"}
expected, _, err := makeInput(command)
if err != nil {
t.Fatal(err)
}
for _, payload := range []string{
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","resume":""}`,
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","workspaceId":"psd"}`,
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","dryRun":false}`,
`{"schemaVersion":1,"operation":"evidence","workspaceId":"psd","resume":null}`,
} {
if err := validateIngress([]byte(payload), expected); err == nil {
t.Errorf("accepted non-derived envelope %s", payload)
}
}
}
func TestValidateIngressRequiresExactDerivedFieldSetForOptionalInputs(t *testing.T) {
command := SuggestFksRequest{WorkspaceID: "psd"}
expected, _, err := makeInput(command)
if err != nil {
t.Fatal(err)
}
payload := `{"schemaVersion":1,"operation":"suggest-fks","workspaceId":"psd","sql":null,"assume":[]}`
if err := validateIngress([]byte(payload), expected); err == nil {
t.Fatal("accepted explicit null/empty optional fields omitted by command")
}
}
func TestPublishCandidateValidatesInternalExportWithoutOutput(t *testing.T) {
candidate := []byte("candidates: []\n")
digest := DigestBytes(candidate)
result := validWorkspaceResult("succeeded", "ok")
result.Operation = "suggest-fks"
result.RunID = strings.Repeat("b", 32)
result.ArtifactIdentities = []ArtifactIdentity{{Kind: "fk-candidates", Digest: digest}}
for _, export := range []hostExport{
{MediaType: "application/json", SHA256: digest, ContentBase64: base64.StdEncoding.EncodeToString(candidate)},
{MediaType: "application/yaml", SHA256: digest, ContentBase64: base64.StdEncoding.EncodeToString(candidate)},
} {
if export.MediaType == "application/json" {
if err := publishCandidate(&export, result, ""); err == nil {
t.Fatal("accepted invalid media type without output")
}
} else if err := publishCandidate(&export, result, ""); err != nil {
t.Fatal(err)
}
}
}
func TestValidateResultBindsStatusCodeAndExit(t *testing.T) {
cases := []struct {
status, code string
exit int
valid bool
}{
{"succeeded", "ok", 0, true}, {"unchanged", "ok", 0, true}, {"dry_run", "ok", 0, true},
{"blocked", "manual_review_required", 3, true}, {"failed", "workspace_not_found", 1, true},
{"blocked", "manual_review_required", 0, false}, {"failed", "workspace_not_found", 0, false},
{"succeeded", "workspace_not_found", 0, false}, {"failed", "ok", 1, false},
}
for _, tc := range cases {
r := validWorkspaceResult(tc.status, tc.code)
err := validateResult(r, "psd", "evidence")
got := err == nil && resultExitMatches(tc.status, tc.exit)
if got != tc.valid {
t.Errorf("%s/%s exit %d valid=%v err=%v", tc.status, tc.code, tc.exit, tc.valid, err)
}
}
}
func TestRunRejectsUnknownHostExportFieldWithoutOutput(t *testing.T) {
d := t.TempDir()
candidate := []byte("candidates: []\n")
digest := DigestBytes(candidate)
response := `{"schemaVersion":1,"status":"succeeded","code":"ok","workspaceId":"psd","workspaceRevision":"` + strings.Repeat("0", 40) + `","descriptorBlob":"` + strings.Repeat("a", 40) + `","operation":"suggest-fks","runId":"` + strings.Repeat("b", 32) + `","completedStages":[],"artifactIdentities":[{"kind":"fk-candidates","digest":"` + digest + `"}],"hostExport":{"mediaType":"application/yaml","sha256":"` + digest + `","contentBase64":"` + base64.StdEncoding.EncodeToString(candidate) + `","extra":1}}`
fake := filepath.Join(d, "docker")
if err := os.WriteFile(fake, []byte("#!/bin/sh\ncat >/dev/null\nprintf '%s' '"+response+"'\n"), 0o700); err != nil {
t.Fatal(err)
}
cmd := SuggestFksRequest{WorkspaceID: "psd"}
if _, err := Run(context.Background(), config.Installation{ProjectDirectory: d}, compose.NewRunner(fake), cmd, nil); err == nil {
t.Fatal("accepted unknown hostExport field")
}
}
func TestPublishCandidateRejectsOversizedExportWithoutOutput(t *testing.T) {
candidate := bytes.Repeat([]byte("y"), maxCandidate+1)
digest := DigestBytes(candidate)
x := hostExport{MediaType: "application/yaml", SHA256: digest, ContentBase64: base64.StdEncoding.EncodeToString(candidate)}
r := validWorkspaceResult("succeeded", "ok")
r.Operation, r.RunID = "suggest-fks", strings.Repeat("b", 32)
if err := publishCandidate(&x, r, ""); err == nil {
t.Fatal("accepted oversized export without output")
}
}
func TestRunAcceptsOnlyMatchingChildExitForStatus(t *testing.T) {
for _, tc := range []struct {
name, status, code string
exit int
wantErr bool
}{
{"blocked-exit3", "blocked", "manual_review_required", 3, false},
{"blocked-exit0", "blocked", "manual_review_required", 0, true},
{"failed-exit1", "failed", "workspace_not_found", 1, false},
{"failed-exit0", "failed", "workspace_not_found", 0, true},
} {
t.Run(tc.name, func(t *testing.T) {
d := t.TempDir()
response := fmt.Sprintf(`{"schemaVersion":1,"status":"%s","code":"%s","workspaceId":"psd","workspaceRevision":"%s","descriptorBlob":"%s","operation":"evidence","completedStages":[]}`,
tc.status, tc.code, strings.Repeat("0", 40), strings.Repeat("a", 40))
fake := filepath.Join(d, "docker")
if err := os.WriteFile(fake, []byte("#!/bin/sh\ncat >/dev/null\nprintf '%s' '"+response+"'\nexit "+fmt.Sprint(tc.exit)+"\n"), 0o700); err != nil {
t.Fatal(err)
}
_, err := Run(context.Background(), config.Installation{ProjectDirectory: d}, compose.NewRunner(fake), EvidenceRequest{WorkspaceID: "psd"}, nil)
if (err != nil) != tc.wantErr {
t.Fatalf("error=%v wantErr=%v", err, tc.wantErr)
}
})
}
}
func TestParseWorkspaceRejectsDuplicateMissingAndUnknownOptions(t *testing.T) {
validResume := strings.Repeat("0", 32)
for _, argv := range [][]string{
{"workspace", "inspect", "--json", "--json", "--workspace", "psd"},
{"workspace", "inspect", "--workspace", "psd", "--workspace", "psd"},
{"workspace", "preprocess", "dwh", "--workspace", "psd", "--resume", validResume, "--resume", validResume},
{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--output", "a", "--output", "b"},
{"workspace", "inspect", "--json", "--unknown", "x", "--workspace", "psd"},
{"workspace", "inspect", "--json"},
} {
if _, err := ParseWorkspaceCommand(argv); err == nil {
t.Errorf("accepted invalid argv %v", argv)
}
}
}
func TestParseWorkspaceAcceptsAssumptionAndEnforcesLimits(t *testing.T) {
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", "orders.id=customers.id"}); err != nil {
t.Fatal(err)
}
if _, err := ParseWorkspaceCommand([]string{"workspace", "schema", "suggest-fks", "--workspace", "psd", "--assume", strings.Repeat("a", maxAssumptionBytes+1)}); err == nil {
t.Fatal("accepted oversized assumption")
}
args := []string{"workspace", "schema", "suggest-fks", "--workspace", "psd"}
for i := 0; i < 33; i++ {
args = append(args, "--from-sql", fmt.Sprintf("schema-%d.sql", i))
}
if _, err := ParseWorkspaceCommand(args); err == nil {
t.Fatal("accepted 33 SQL files")
}
}