fix(drive): preserve parent token on nested overwrite (#908)

* fix(drive): preserve parent token on nested overwrite

Ensure drive +push overwrite requests for nested files keep parent_node aligned with the actual remote parent folder and report parent resolution failures explicitly.

* test(drive): cover nested overwrite push workflow

Add a live drive +push workflow case for overwriting a nested remote file so the PR parent-token fix is exercised against the real backend and verified to converge via +status.
This commit is contained in:
fangshuyu-768
2026-05-15 18:32:58 +08:00
committed by GitHub
parent 7400226e34
commit 5778adfefa
3 changed files with 201 additions and 1 deletions

View File

@@ -275,7 +275,14 @@ var DrivePush = common.Shortcut{
skipped++
continue
}
token, version, upErr := drivePushUploadFile(ctx, runtime, localFile, entry.FileToken, folderToken)
parentToken, parentErr := drivePushEnsureParentToken(ctx, runtime, folderToken, rel, folderCache)
if parentErr != nil {
items = append(items, drivePushItem{RelPath: rel, FileToken: entry.FileToken, Action: "failed", SizeBytes: localFile.Size, Error: parentErr.Error()})
failed++
uploadFailed = true
continue
}
token, version, upErr := drivePushUploadFile(ctx, runtime, localFile, entry.FileToken, parentToken)
if upErr != nil {
// Token contract on overwrite failure: an in-place
// overwrite preserves the file's token, so the
@@ -580,6 +587,10 @@ func drivePushEnsureFolder(ctx context.Context, runtime *common.RuntimeContext,
return token, nil
}
func drivePushEnsureParentToken(ctx context.Context, runtime *common.RuntimeContext, rootFolderToken, relPath string, folderCache map[string]string) (string, error) {
return drivePushEnsureFolder(ctx, runtime, rootFolderToken, drivePushParentRel(relPath), folderCache)
}
// drivePushUploadFile uploads (or overwrites) a single local file. When
// existingToken is non-empty, the request adds the file_token form field to
// trigger overwrite-with-version semantics on the backend; the response is

View File

@@ -1296,6 +1296,130 @@ func TestDrivePushReusesExistingRemoteFolder(t *testing.T) {
}
}
// TestDrivePushOverwriteNestedFileUsesParentFolderToken verifies that
// overwriting an existing nested remote file keeps parent_node aligned with
// the file's actual parent folder instead of the root folder token.
func TestDrivePushOverwriteNestedFileUsesParentFolderToken(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "fld_existing_sub", "name": "sub", "type": "folder"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=fld_existing_sub",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_nested", "name": "keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
uploadStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/upload_all",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"file_token": "tok_keep_nested",
"version": "v2",
},
},
}
reg.Register(uploadStub)
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v\nstdout: %s", err, stdout.String())
}
body := decodeDriveMultipartBody(t, uploadStub)
if got := body.Fields["file_token"]; got != "tok_keep_nested" {
t.Fatalf("upload_all file_token = %q, want tok_keep_nested", got)
}
if got := body.Fields["parent_node"]; got != "fld_existing_sub" {
t.Fatalf("upload_all parent_node = %q, want fld_existing_sub", got)
}
}
func TestDrivePushOverwriteNestedFileReportsParentEnsureFailure(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
tmpDir := t.TempDir()
withDriveWorkingDir(t, tmpDir)
if err := os.MkdirAll(filepath.Join("local", "sub"), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join("local", "sub", "keep.txt"), []byte("local"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "folder_token=folder_root",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"data": map[string]interface{}{
"files": []interface{}{
map[string]interface{}{"token": "tok_keep_nested", "name": "sub/keep.txt", "type": "file"},
},
"has_more": false,
},
},
})
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/drive/v1/files/create_folder",
Body: map[string]interface{}{
"code": 9999,
"msg": "create parent failed",
},
})
err := mountAndRunDrive(t, DrivePush, []string{
"+push",
"--local-dir", "local",
"--folder-token", "folder_root",
"--if-exists", "overwrite",
"--as", "bot",
}, f, stdout)
if err == nil {
t.Fatalf("expected parent ensure failure\nstdout: %s", stdout.String())
}
if !strings.Contains(stdout.String(), `"action": "failed"`) || !strings.Contains(stdout.String(), "create parent failed") {
t.Fatalf("expected failed item with create_folder error, got: %s", stdout.String())
}
}
// TestDrivePushMirrorsEmptyDirectories confirms the gap codex review
// flagged: a local directory with no files inside must still surface on
// Drive as a created sub-folder, not be silently dropped because the

View File

@@ -207,4 +207,69 @@ func TestDrive_DuplicateRemoteWorkflow(t *testing.T) {
t.Fatalf("+status should converge to a clean unchanged mirror\nstdout:\n%s", statusResult.Stdout)
}
})
t.Run("push overwrites nested remote file under its real parent", func(t *testing.T) {
suffix := clie2e.GenerateSuffix()
folderToken := createDriveFolder(t, parentT, ctx, "lark-cli-e2e-drive-nested-push-"+suffix, "")
subFolderToken := createDriveFolder(t, parentT, ctx, "sub", folderToken)
workDir := t.TempDir()
if err := os.MkdirAll(filepath.Join(workDir, "local", "sub"), 0o755); err != nil {
t.Fatalf("mkdir local/sub: %v", err)
}
if err := os.WriteFile(filepath.Join(workDir, "local", "sub", "keep.txt"), []byte("local-nested-overwrite"), 0o644); err != nil {
t.Fatalf("write local/sub/keep.txt: %v", err)
}
existingToken := uploadNamedFile(t, workDir, subFolderToken, "_nested_keep.txt", "keep.txt", "remote-before")
pushResult, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"drive", "+push",
"--local-dir", "local",
"--folder-token", folderToken,
"--if-exists", "overwrite",
},
WorkDir: workDir,
DefaultAs: "bot",
})
require.NoError(t, err)
pushResult.AssertExitCode(t, 0)
pushResult.AssertStdoutStatus(t, true)
if got := gjson.Get(pushResult.Stdout, "data.summary.uploaded").Int(); got != 1 {
t.Fatalf("nested +push uploaded=%d, want 1\nstdout:\n%s", got, pushResult.Stdout)
}
if got := gjson.Get(pushResult.Stdout, `data.items.#(rel_path="sub/keep.txt").action`).String(); got != "overwritten" {
t.Fatalf("nested +push action=%q, want overwritten\nstdout:\n%s", got, pushResult.Stdout)
}
if got := gjson.Get(pushResult.Stdout, `data.items.#(rel_path="sub/keep.txt").file_token`).String(); got != existingToken {
t.Fatalf("nested +push file_token=%q, want existing token %q\nstdout:\n%s", got, existingToken, pushResult.Stdout)
}
statusResult, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"drive", "+status",
"--local-dir", "local",
"--folder-token", folderToken,
},
WorkDir: workDir,
DefaultAs: "bot",
})
require.NoError(t, err)
skipDriveStatusExactIfMissingDownloadScope(t, statusResult)
statusResult.AssertExitCode(t, 0)
statusResult.AssertStdoutStatus(t, true)
if got := gjson.Get(statusResult.Stdout, "data.unchanged.#").Int(); got != 1 {
t.Fatalf("nested +status unchanged count=%d, want 1\nstdout:\n%s", got, statusResult.Stdout)
}
if got := gjson.Get(statusResult.Stdout, "data.unchanged.0.rel_path").String(); got != "sub/keep.txt" {
t.Fatalf("nested +status unchanged rel_path=%q, want sub/keep.txt\nstdout:\n%s", got, statusResult.Stdout)
}
if got := gjson.Get(statusResult.Stdout, "data.modified.#").Int(); got != 0 ||
gjson.Get(statusResult.Stdout, "data.new_local.#").Int() != 0 ||
gjson.Get(statusResult.Stdout, "data.new_remote.#").Int() != 0 {
t.Fatalf("nested overwrite should converge to a clean unchanged mirror\nstdout:\n%s", statusResult.Stdout)
}
})
}