Pin the blobstore fallback and cover cross-instance uploads
Why: an unset object store must keep artifact bytes available to the other process, which the store-level test does not execute. Evidence: cross_artifact_roundtrip failed with EOF on backend c6fb130 and passed on 46073d30 (SELF, isolated u7drill-k4). Impact: Dev now builds the fixed backend. Second-instance write traffic stays closed. Refs: modules/backend tests/integration/multi-instance/multi_instance_test.go Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
Submodule modules/backend updated: 0f192a4813...46073d30a5
@@ -15,10 +15,12 @@ package multiinstance
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
@@ -123,6 +125,19 @@ func TestTwoInstancesOverSharedPG(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cross_artifact_roundtrip", func(t *testing.T) {
|
||||
// 未配置对象存储时,A 上传的正文必须落在共享本地目录,B 原样取回。
|
||||
payload := []byte("k4-cross-instance-artifact")
|
||||
id := e.uploadArtifact(t, uc.api, projID, "k4.txt", payload)
|
||||
code, raw := e.raw(t, uc.apiB, "GET", "/api/artifacts/"+id+"/content", nil)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("B artifact content: status %d, body %.300s", code, raw)
|
||||
}
|
||||
if sha256.Sum256([]byte(raw)) != sha256.Sum256(payload) {
|
||||
t.Fatalf("B downloaded different bytes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cross_event_sse", func(t *testing.T) {
|
||||
lastSeq := e.lastSeq(t, uc.apiB, convID)
|
||||
events := e.subscribe(t, uc.streamB, convID, lastSeq)
|
||||
@@ -313,6 +328,9 @@ func (e *env) startInstance(t *testing.T, name, addr string) *instance {
|
||||
)
|
||||
log := &lockedBuffer{}
|
||||
cmd.Stdout, cmd.Stderr = log, log
|
||||
// 演练环境可能带有空的对象存储变量。子进程必须按「未配置」启动,
|
||||
// 否则测不到带类型空 *S3 回落本地目录的路径。
|
||||
cmd.Env = withoutBlobstoreS3(os.Environ())
|
||||
// 独立 cwd:serve 会向 ./data 写 skills/SQLite 等,不能落在测试目录。
|
||||
cmd.Dir = t.TempDir()
|
||||
if err := cmd.Start(); err != nil {
|
||||
@@ -324,6 +342,52 @@ func (e *env) startInstance(t *testing.T, name, addr string) *instance {
|
||||
return inst
|
||||
}
|
||||
|
||||
func withoutBlobstoreS3(env []string) []string {
|
||||
out := make([]string, 0, len(env))
|
||||
for _, item := range env {
|
||||
if strings.HasPrefix(item, "BLOBSTORE_S3_") {
|
||||
continue
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *env) uploadArtifact(t *testing.T, c *http.Client, projectID, name string, payload []byte) string {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
part, err := writer.CreateFormFile("file", name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := part.Write(payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, e.baseFor(c)+"/api/projects/"+projectID+"/uploads", &buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("upload: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
var info struct {
|
||||
ArtifactID string `json:"artifactId"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &info)
|
||||
if resp.StatusCode != http.StatusOK || info.ArtifactID == "" {
|
||||
t.Fatalf("upload status %d id %q body %.300s", resp.StatusCode, info.ArtifactID, raw)
|
||||
}
|
||||
return info.ArtifactID
|
||||
}
|
||||
|
||||
func (e *env) waitHealthy(t *testing.T, inst *instance) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(60 * time.Second)
|
||||
|
||||
Reference in New Issue
Block a user