Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b68450709 | ||
|
|
346444ea38 | ||
|
|
b469bb127a | ||
|
|
c6e810e4d9 | ||
|
|
98d03455b1 | ||
|
|
fad41d4d99 | ||
|
|
5fed80fc0f | ||
|
|
1727025f67 | ||
|
|
715f5346da | ||
|
|
bfd836064d |
@@ -1,6 +1,6 @@
|
||||
# CLI flag 兼容迁移治理
|
||||
|
||||
本文定义一种受控迁移:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 提升为必填。它只解决这一种精确变更,不是通用 breaking-change 豁免。
|
||||
本文定义一种受控迁移:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 设为唯一可见入口。迁移必须保持原 flag 的 requiredness:optional 只能迁到 optional,required 只能迁到 required。它只解决这一种精确变更,不是通用 breaking-change 豁免。
|
||||
|
||||
同名 flag 的精确类型迁移属于另一类评审机制,只能进入
|
||||
`internal/interfacesnapshot/reviewed.go` 与 legacy smoke helper 的镜像表;flag rename
|
||||
@@ -50,7 +50,7 @@ scripts/policy/interface-migrations/approved-flag-migrations-v1.json
|
||||
| 阶段 | PR 可以做什么 | 必须满足的快照状态 |
|
||||
|---|---|---|
|
||||
| 1. 治理审批 | 新增 `state: pending` 的精确记录;不得在同一个 PR 修改产品 surface | candidate 和 merge-base 都与记录中的 `before` 完全一致;该记录不改变 stable 的判断 |
|
||||
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | legacy 仍存在但由 visible 变 hidden,且声明 `alias_of`;canonical 达到记录的必填状态 |
|
||||
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | legacy 仍存在但由 visible 变 hidden,且声明 `alias_of`;canonical 的 requiredness 与 legacy 迁移前完全一致 |
|
||||
| 3. 保留回执 | 产品 PR 合入后,如果 stable 仍是 `before`,继续保留 `consumed` | merge-base 或 stable 仍有任一份尚未达到 `after` |
|
||||
| 4. 单独清理 | 当 merge-base 和 stable 都已经是 `after`,在后续 PR 删除该记录 | 两份参考快照均精确匹配 `after`;继续保留过期回执会被门禁拒绝 |
|
||||
|
||||
@@ -122,7 +122,7 @@ scripts/policy/interface-migrations/approved-flag-migrations-v1.json
|
||||
一条 base-owned、状态正确且前后快照精确匹配的记录,只会从普通兼容报告中移除以下两类预期 finding:
|
||||
|
||||
1. legacy flag 的 `flag_became_hidden`(visible → hidden);
|
||||
2. canonical flag 的 `required_flag_added`(新增时即必填)或 `flag_became_required`(已有 flag 从可选变必填)。
|
||||
2. required legacy 被新增的 required canonical 替代时产生的 `required_flag_added`;如果 canonical 在 before 阶段只是 hidden 占位符,则允许它在转为公开拼写时继承 legacy 的 requiredness。已有的 visible canonical 不允许借 rename 改变 requiredness。
|
||||
|
||||
以下变化仍按普通兼容规则阻塞,不能被迁移记录掩盖:
|
||||
|
||||
@@ -145,7 +145,7 @@ legacy 名改为 canonical 名。Schema adapter 只接受已经由三方 Interfa
|
||||
`required` / `cli_required` 或重写 constraint;
|
||||
- rename 前后的 `type`、`property`、`interface_type`、default、format、enum 与
|
||||
`required_when` 必须完全一致;
|
||||
- `required` / `cli_required` 只能保持不变或按审批从 `false` 提升为 `true`,禁止降低;
|
||||
- `required` / `cli_required` 必须在 rename 前后完全一致,升高或降低都失败;
|
||||
- constraint 只允许在同一 tool 内按已枚举的 legacy → canonical map 做 member 替换、
|
||||
排序与去重;group kind、非迁移 member 或 group 增删仍然阻塞;
|
||||
- 多个 legacy 指向同一 canonical 时,所有历史 parameter signature 必须一致,否则
|
||||
|
||||
@@ -324,11 +324,23 @@ func (m FlagMigration) validate() error {
|
||||
if m.Canonical.After.Hidden {
|
||||
return fmt.Errorf("canonical flag must remain visible")
|
||||
}
|
||||
if !m.Canonical.After.Required {
|
||||
return fmt.Errorf("canonical flag must be required after migration")
|
||||
// Requiredness belongs to the one logical parameter. The hidden legacy
|
||||
// spelling must not remain independently required, while the canonical
|
||||
// spelling inherits the exact before-state contract. An already-visible
|
||||
// canonical flag cannot change requiredness; an existing hidden canonical
|
||||
// placeholder may inherit it when promoted to the public spelling.
|
||||
if m.Legacy.After.Required {
|
||||
return fmt.Errorf("legacy compatibility alias must not remain independently required after migration")
|
||||
}
|
||||
if m.Canonical.Before.Present && m.Canonical.Before.Required {
|
||||
return fmt.Errorf("canonical flag must be absent or optional before migration")
|
||||
if m.Legacy.Before.Required != m.Canonical.After.Required {
|
||||
return fmt.Errorf(
|
||||
"flag requiredness must be preserved from legacy before to canonical after",
|
||||
)
|
||||
}
|
||||
if m.Canonical.Before.Present &&
|
||||
!m.Canonical.Before.Hidden &&
|
||||
m.Canonical.Before.Required != m.Canonical.After.Required {
|
||||
return fmt.Errorf("canonical flag requiredness must remain unchanged when already present")
|
||||
}
|
||||
if m.Legacy.After.AliasOf != m.Canonical.Name {
|
||||
return fmt.Errorf(
|
||||
@@ -682,6 +694,7 @@ func flagMigrationAuthorizesChange(
|
||||
return true
|
||||
}
|
||||
if migration.Canonical.Before.Present &&
|
||||
migration.Canonical.Before.Hidden &&
|
||||
!migration.Canonical.Before.Required &&
|
||||
migration.Canonical.After.Required &&
|
||||
change.Kind == "flag_became_required" {
|
||||
|
||||
@@ -188,7 +188,7 @@ func TestCrossPlatformCoverageFlagMigrationManifestParserEdges(t *testing.T) {
|
||||
|
||||
func TestCrossPlatformCoverageFlagMigrationManifestRejectsEveryContractDrift(t *testing.T) {
|
||||
optionalCanonical := func() FlagMigrationManifest {
|
||||
manifest := coverageManifest(FlagMigrationPending)
|
||||
manifest := coverageOptionalManifest(FlagMigrationPending)
|
||||
manifest.Migrations[0].Canonical.Before = FlagMigrationState{
|
||||
Present: true,
|
||||
Type: "string",
|
||||
@@ -269,6 +269,36 @@ func TestCrossPlatformCoverageFlagMigrationManifestRejectsEveryContractDrift(t *
|
||||
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After = FlagMigrationState{} },
|
||||
wantErr: "canonical flag must be present after migration",
|
||||
},
|
||||
{
|
||||
name: "required legacy cannot become optional canonical",
|
||||
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
|
||||
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After.Required = false },
|
||||
wantErr: "requiredness must be preserved from legacy before to canonical after",
|
||||
},
|
||||
{
|
||||
name: "optional legacy cannot become required canonical",
|
||||
make: func() FlagMigrationManifest { return coverageOptionalManifest(FlagMigrationPending) },
|
||||
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After.Required = true },
|
||||
wantErr: "requiredness must be preserved from legacy before to canonical after",
|
||||
},
|
||||
{
|
||||
name: "existing canonical cannot change requiredness",
|
||||
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
|
||||
mutate: func(manifest *FlagMigrationManifest) {
|
||||
manifest.Migrations[0].Canonical.Before = FlagMigrationState{
|
||||
Present: true,
|
||||
Type: "string",
|
||||
Scope: "local",
|
||||
}
|
||||
},
|
||||
wantErr: "canonical flag requiredness must remain unchanged when already present",
|
||||
},
|
||||
{
|
||||
name: "hidden legacy alias is not independently required",
|
||||
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
|
||||
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.Required = true },
|
||||
wantErr: "legacy compatibility alias must not remain independently required",
|
||||
},
|
||||
{
|
||||
name: "legacy after declares alias target",
|
||||
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
|
||||
@@ -711,8 +741,32 @@ func TestCrossPlatformCoverageCompareAllWithFlagMigrationsInheritedAndOptionalCa
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("existing optional canonical becomes required", func(t *testing.T) {
|
||||
pending := coverageManifest(FlagMigrationPending)
|
||||
t.Run("optional canonical is introduced without becoming required", func(t *testing.T) {
|
||||
pending := coverageOptionalManifest(FlagMigrationPending)
|
||||
consumed := coverageOptionalManifest(FlagMigrationConsumed)
|
||||
before := coverageMigrationSnapshot(pending.Migrations[0], false, false)
|
||||
after := coverageMigrationSnapshot(pending.Migrations[0], true, false)
|
||||
|
||||
ordinary := Compare(after, before, "merge-base")
|
||||
if !hasFlagChange(ordinary.Blocking, "flag_became_hidden", pending.Migrations[0].Command, pending.Migrations[0].Legacy.Name) {
|
||||
t.Fatalf("fixture did not create flag_became_hidden: %#v", ordinary.Blocking)
|
||||
}
|
||||
if hasFlagChange(ordinary.Blocking, "required_flag_added", pending.Migrations[0].Command, pending.Migrations[0].Canonical.Name) {
|
||||
t.Fatalf("optional canonical was treated as required: %#v", ordinary.Blocking)
|
||||
}
|
||||
report, err := CompareAllWithFlagMigrations(
|
||||
after,
|
||||
map[string]Snapshot{"merge-base": before, "stable": before},
|
||||
pending,
|
||||
consumed,
|
||||
)
|
||||
if err != nil || !report.Compatible {
|
||||
t.Fatalf("optional rename = (%#v, %v), want compatible", report, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("existing optional canonical remains optional", func(t *testing.T) {
|
||||
pending := coverageOptionalManifest(FlagMigrationPending)
|
||||
pending.Migrations[0].Canonical.Before = FlagMigrationState{
|
||||
Present: true,
|
||||
Type: "string",
|
||||
@@ -725,8 +779,8 @@ func TestCrossPlatformCoverageCompareAllWithFlagMigrationsInheritedAndOptionalCa
|
||||
after := coverageMigrationSnapshot(pending.Migrations[0], true, false)
|
||||
|
||||
ordinary := Compare(after, before, "merge-base")
|
||||
if !hasFlagChange(ordinary.Blocking, "flag_became_required", pending.Migrations[0].Command, pending.Migrations[0].Canonical.Name) {
|
||||
t.Fatalf("fixture did not create flag_became_required: %#v", ordinary.Blocking)
|
||||
if hasFlagChange(ordinary.Blocking, "flag_became_required", pending.Migrations[0].Command, pending.Migrations[0].Canonical.Name) {
|
||||
t.Fatalf("fixture changed canonical requiredness: %#v", ordinary.Blocking)
|
||||
}
|
||||
report, err := CompareAllWithFlagMigrations(
|
||||
after,
|
||||
@@ -735,7 +789,83 @@ func TestCrossPlatformCoverageCompareAllWithFlagMigrationsInheritedAndOptionalCa
|
||||
consumed,
|
||||
)
|
||||
if err != nil || !report.Compatible {
|
||||
t.Fatalf("optional-to-required migration = (%#v, %v), want compatible", report, err)
|
||||
t.Fatalf("existing optional canonical migration = (%#v, %v), want compatible", report, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hidden canonical inherits requiredness when promoted", func(t *testing.T) {
|
||||
pending := coverageManifest(FlagMigrationPending)
|
||||
pending.Migrations[0].Canonical.Before = FlagMigrationState{
|
||||
Present: true,
|
||||
Type: "string",
|
||||
Hidden: true,
|
||||
Scope: "local",
|
||||
}
|
||||
consumed := pending
|
||||
consumed.Migrations = append([]FlagMigration(nil), pending.Migrations...)
|
||||
consumed.Migrations[0].State = FlagMigrationConsumed
|
||||
before := coverageMigrationSnapshot(pending.Migrations[0], false, false)
|
||||
after := coverageMigrationSnapshot(pending.Migrations[0], true, false)
|
||||
|
||||
ordinary := Compare(after, before, "merge-base")
|
||||
if !hasFlagChange(ordinary.Blocking, "flag_became_required", pending.Migrations[0].Command, pending.Migrations[0].Canonical.Name) {
|
||||
t.Fatalf("fixture did not change canonical requiredness: %#v", ordinary.Blocking)
|
||||
}
|
||||
report, err := CompareAllWithFlagMigrations(
|
||||
after,
|
||||
map[string]Snapshot{"merge-base": before, "stable": before},
|
||||
pending,
|
||||
consumed,
|
||||
)
|
||||
if err != nil || !report.Compatible {
|
||||
t.Fatalf("hidden canonical promotion = (%#v, %v), want compatible", report, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageOptionalFlagMigrationLifecycleRemainsHostile(t *testing.T) {
|
||||
pending := coverageOptionalManifest(FlagMigrationPending)
|
||||
consumed := coverageOptionalManifest(FlagMigrationConsumed)
|
||||
empty := coverageEmptyManifest()
|
||||
migration := pending.Migrations[0]
|
||||
before := coverageMigrationSnapshot(migration, false, false)
|
||||
after := coverageMigrationSnapshot(migration, true, false)
|
||||
|
||||
t.Run("candidate cannot self authorize", func(t *testing.T) {
|
||||
_, err := CompareAllWithFlagMigrations(
|
||||
after,
|
||||
map[string]Snapshot{"merge-base": before, "stable": before},
|
||||
empty,
|
||||
pending,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "cannot authorize its own interface change") {
|
||||
t.Fatalf("candidate self-authorization error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("partial application remains rejected", func(t *testing.T) {
|
||||
partial := coverageMigrationSnapshot(migration, false, false)
|
||||
partial.Commands[len(partial.Commands)-1].LocalFlags[0].Hidden = true
|
||||
_, err := CompareAllWithFlagMigrations(
|
||||
partial,
|
||||
map[string]Snapshot{"merge-base": before, "stable": before},
|
||||
pending,
|
||||
consumed,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "partially applied flag migration") {
|
||||
t.Fatalf("partial optional migration error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("consumed receipt remains stale after every reference converges", func(t *testing.T) {
|
||||
_, err := CompareAllWithFlagMigrations(
|
||||
after,
|
||||
map[string]Snapshot{"merge-base": after, "stable": after},
|
||||
consumed,
|
||||
consumed,
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), "stale after all references reached the after state") {
|
||||
t.Fatalf("stale optional migration error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -886,6 +1016,13 @@ func coverageManifest(state string) FlagMigrationManifest {
|
||||
}
|
||||
}
|
||||
|
||||
func coverageOptionalManifest(state string) FlagMigrationManifest {
|
||||
manifest := coverageManifest(state)
|
||||
manifest.Migrations[0].Legacy.Before.Required = false
|
||||
manifest.Migrations[0].Canonical.After.Required = false
|
||||
return manifest
|
||||
}
|
||||
|
||||
func coverageEmptyManifest() FlagMigrationManifest {
|
||||
return FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package interfacesnapshot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -37,6 +38,42 @@ const validFlagMigrationManifestJSON = `{
|
||||
]
|
||||
}`
|
||||
|
||||
func optionalFlagMigrationManifestJSON() string {
|
||||
manifest := strings.Replace(
|
||||
validFlagMigrationManifestJSON,
|
||||
`"before": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
||||
`"before": {"present": true, "type": "string", "scope": "local"}`,
|
||||
1,
|
||||
)
|
||||
return strings.Replace(
|
||||
manifest,
|
||||
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
||||
`"after": {"present": true, "type": "string", "scope": "local"}`,
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
func hiddenCanonicalFlagMigrationManifestJSON() string {
|
||||
return strings.Replace(
|
||||
validFlagMigrationManifestJSON,
|
||||
`"before": {"present": false}`,
|
||||
`"before": {"present": true, "type": "string", "hidden": true, "scope": "local"}`,
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
func TestApprovedFlagMigrationManifestRemainsValid(t *testing.T) {
|
||||
manifest, err := os.Open("../../scripts/policy/interface-migrations/approved-flag-migrations-v1.json")
|
||||
if err != nil {
|
||||
t.Fatalf("open approved flag migration manifest: %v", err)
|
||||
}
|
||||
defer manifest.Close()
|
||||
|
||||
if _, err := ReadFlagMigrationManifest(manifest); err != nil {
|
||||
t.Fatalf("approved flag migration manifest is invalid: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsUnknownFields(t *testing.T) {
|
||||
_, err := ReadFlagMigrationManifest(strings.NewReader(`{
|
||||
"version": 1,
|
||||
@@ -110,6 +147,12 @@ func TestCrossPlatformCoverageReadFlagMigrationManifestValidatesExactEntries(t *
|
||||
if _, err := ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON)); err != nil {
|
||||
t.Fatalf("ReadFlagMigrationManifest(valid) error = %v", err)
|
||||
}
|
||||
if _, err := ReadFlagMigrationManifest(strings.NewReader(optionalFlagMigrationManifestJSON())); err != nil {
|
||||
t.Fatalf("ReadFlagMigrationManifest(optional rename) error = %v", err)
|
||||
}
|
||||
if _, err := ReadFlagMigrationManifest(strings.NewReader(hiddenCanonicalFlagMigrationManifestJSON())); err != nil {
|
||||
t.Fatalf("ReadFlagMigrationManifest(hidden canonical promotion) error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -162,24 +205,34 @@ func TestCrossPlatformCoverageReadFlagMigrationManifestValidatesExactEntries(t *
|
||||
wantErr: "canonical flag must remain visible",
|
||||
},
|
||||
{
|
||||
name: "canonical remains optional",
|
||||
name: "required legacy becomes optional canonical",
|
||||
input: strings.Replace(
|
||||
validFlagMigrationManifestJSON,
|
||||
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
||||
`"after": {"present": true, "type": "string", "scope": "local"}`,
|
||||
1,
|
||||
),
|
||||
wantErr: "canonical flag must be required after migration",
|
||||
wantErr: "requiredness must be preserved from legacy before to canonical after",
|
||||
},
|
||||
{
|
||||
name: "canonical was already required",
|
||||
name: "optional legacy becomes required canonical",
|
||||
input: strings.Replace(
|
||||
optionalFlagMigrationManifestJSON(),
|
||||
`"after": {"present": true, "type": "string", "scope": "local"}`,
|
||||
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
||||
1,
|
||||
),
|
||||
wantErr: "requiredness must be preserved from legacy before to canonical after",
|
||||
},
|
||||
{
|
||||
name: "existing canonical changes requiredness",
|
||||
input: strings.Replace(
|
||||
validFlagMigrationManifestJSON,
|
||||
`"before": {"present": false}`,
|
||||
`"before": {"present": true, "type": "string", "required": true, "scope": "local"}`,
|
||||
`"before": {"present": true, "type": "string", "scope": "local"}`,
|
||||
1,
|
||||
),
|
||||
wantErr: "canonical flag must be absent or optional before migration",
|
||||
wantErr: "canonical flag requiredness must remain unchanged when already present",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -215,9 +268,10 @@ func TestCrossPlatformCoverageFlagMigrationManifestRejectsDuplicateAndInexactCon
|
||||
canonicalDrift := manifest
|
||||
canonicalDrift.Migrations = append([]FlagMigration(nil), manifest.Migrations...)
|
||||
canonicalDrift.Migrations[0].Canonical.Before = FlagMigrationState{
|
||||
Present: true,
|
||||
Type: "string",
|
||||
Scope: "local",
|
||||
Present: true,
|
||||
Type: "string",
|
||||
Required: true,
|
||||
Scope: "local",
|
||||
}
|
||||
canonicalDrift.Migrations[0].Canonical.After.Type = "stringSlice"
|
||||
if err := canonicalDrift.Validate(); err == nil || !strings.Contains(err.Error(), "canonical flag type") {
|
||||
|
||||
@@ -486,9 +486,23 @@ var Upload = shortcut.Shortcut{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if remoteName := firstString(verified, "name", "fileName"); remoteName == "" || !strings.HasPrefix(remoteName, strings.TrimSuffix(name, filepath.Ext(name))) {
|
||||
remoteID := firstString(verified, "fileId", "dentryUuid", "nodeId", "id")
|
||||
if remoteID == "" {
|
||||
return driveResponseError("drive/commit_upload", "readback_missing_id", "上传后读回缺少文件 ID;无法证明读回的是已提交文件")
|
||||
}
|
||||
if remoteID != nodeID {
|
||||
return driveResponseError("drive/commit_upload", "readback_id_mismatch", fmt.Sprintf("上传后读回文件 ID %q 与提交 ID %q 不一致", remoteID, nodeID))
|
||||
}
|
||||
if remoteName := firstString(verified, "name", "fileName"); !driveReadbackNameMatches(verified, name) {
|
||||
return driveResponseError("drive/commit_upload", "readback_mismatch", fmt.Sprintf("上传后读回名称 %q 与请求 %q 不一致", remoteName, name))
|
||||
}
|
||||
remoteSize, ok := firstInt64(verified, "fileSize", "size", "byteSize", "length")
|
||||
if !ok {
|
||||
return driveResponseError("drive/commit_upload", "readback_missing_size", "上传后读回缺少有效文件大小;无法证明远端文件完整")
|
||||
}
|
||||
if remoteSize != info.Size() {
|
||||
return driveResponseError("drive/commit_upload", "readback_size_mismatch", fmt.Sprintf("上传后读回大小 %d 与本地文件大小 %d 不一致", remoteSize, info.Size()))
|
||||
}
|
||||
return rt.Output(map[string]any{"success": true, "nodeId": nodeID, "sizeBytes": info.Size(), "file": verified})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ package drive
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
@@ -216,6 +218,47 @@ func nestedString(data map[string]any, keys ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func driveReadbackNameMatches(data map[string]any, requested string) bool {
|
||||
remoteName := firstString(data, "name", "fileName")
|
||||
if remoteName == requested {
|
||||
return true
|
||||
}
|
||||
extension := strings.TrimLeft(firstString(data, "extension", "fileExtension", "ext"), ".")
|
||||
return extension != "" && remoteName+"."+extension == requested
|
||||
}
|
||||
|
||||
func firstInt64(data map[string]any, keys ...string) (int64, bool) {
|
||||
for _, key := range keys {
|
||||
value, present := data[key]
|
||||
if !present {
|
||||
continue
|
||||
}
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return int64(typed), true
|
||||
case int32:
|
||||
return int64(typed), true
|
||||
case int64:
|
||||
return typed, true
|
||||
case float64:
|
||||
if !math.IsNaN(typed) && !math.IsInf(typed, 0) && typed == math.Trunc(typed) && typed >= math.MinInt64 && typed < math.MaxInt64 {
|
||||
return int64(typed), true
|
||||
}
|
||||
case json.Number:
|
||||
parsed, err := strconv.ParseInt(typed.String(), 10, 64)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
case string:
|
||||
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
if err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func driveResponseError(operation, reason, message string) error {
|
||||
return apperrors.NewAPI(message,
|
||||
apperrors.WithOperation(operation),
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package drive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
|
||||
@@ -546,6 +547,31 @@ var Move = shortcut.Shortcut{
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remoteID := firstString(verified, "nodeId", "fileId", "dentryUuid", "id")
|
||||
if remoteID == "" {
|
||||
return driveResponseError("doc/move_document", "readback_missing_id", "移动后读回缺少节点 ID;无法证明读回的是已移动节点")
|
||||
}
|
||||
if remoteID != rt.Str("node") {
|
||||
return driveResponseError("doc/move_document", "readback_id_mismatch", fmt.Sprintf("移动后读回节点 %q 与请求节点 %q 不一致", remoteID, rt.Str("node")))
|
||||
}
|
||||
if rt.Changed("folder") {
|
||||
remoteFolder := firstString(verified, "folderId", "targetFolderId", "parentId")
|
||||
if remoteFolder == "" {
|
||||
return driveResponseError("doc/move_document", "readback_missing_folder", "移动后读回缺少目标文件夹 ID;无法证明移动已到达请求位置")
|
||||
}
|
||||
if remoteFolder != rt.Str("folder") {
|
||||
return driveResponseError("doc/move_document", "readback_folder_mismatch", fmt.Sprintf("移动后读回文件夹 %q 与请求 %q 不一致", remoteFolder, rt.Str("folder")))
|
||||
}
|
||||
}
|
||||
if rt.Changed("workspace") {
|
||||
remoteWorkspace := firstString(verified, "workspaceId", "spaceId")
|
||||
if remoteWorkspace == "" {
|
||||
return driveResponseError("doc/move_document", "readback_missing_workspace", "移动后读回缺少目标知识库 ID;无法证明移动已到达请求位置")
|
||||
}
|
||||
if remoteWorkspace != rt.Str("workspace") {
|
||||
return driveResponseError("doc/move_document", "readback_workspace_mismatch", fmt.Sprintf("移动后读回知识库 %q 与请求 %q 不一致", remoteWorkspace, rt.Str("workspace")))
|
||||
}
|
||||
}
|
||||
return rt.Output(map[string]any{"success": true, "nodeId": rt.Str("node"), "file": verified})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -176,6 +177,40 @@ func TestCrossPlatformCoverageDriveDownloadAndUploadRequireArtifactsAndReadback(
|
||||
if _, _, err := resolveDriveUploadInput("../escape.bin"); err == nil {
|
||||
t.Fatal("upload path escape was accepted")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
committedID string
|
||||
readback string
|
||||
want string
|
||||
}{
|
||||
{"missing remote id", "uploaded-2", `{"success":true,"result":{"name":"input.bin","fileSize":18}}`, "缺少文件 ID"},
|
||||
{"mismatched remote id", "uploaded-3", `{"success":true,"result":{"fileId":"other","name":"input.bin","fileSize":18}}`, "与提交 ID"},
|
||||
{"prefix-only remote name", "uploaded-4", `{"success":true,"result":{"fileId":"uploaded-4","name":"input.bin-old","fileSize":18}}`, "读回名称"},
|
||||
{"missing remote size", "uploaded-5", `{"success":true,"result":{"fileId":"uploaded-5","name":"input.bin"}}`, "缺少有效文件大小"},
|
||||
{"mismatched remote size", "uploaded-6", `{"success":true,"result":{"fileId":"uploaded-6","name":"input.bin","fileSize":17}}`, "与本地文件大小 18 不一致"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
testseam.Swap(t, &uploadDriveFile, func(context.Context, helpers.DriveUploadRequest) (map[string]any, error) {
|
||||
return map[string]any{"success": true, "result": map[string]any{"fileId": tc.committedID}}, nil
|
||||
})
|
||||
caller := &driveCoverageCaller{responses: map[string][]string{"get_file_info": {tc.readback}}}
|
||||
err := runDriveCoverage(t, Upload, caller, "--file", "input.bin", "--yes")
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("error = %v, want %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("split remote extension", func(t *testing.T) {
|
||||
testseam.Swap(t, &uploadDriveFile, func(context.Context, helpers.DriveUploadRequest) (map[string]any, error) {
|
||||
return map[string]any{"success": true, "result": map[string]any{"fileId": "uploaded-7"}}, nil
|
||||
})
|
||||
caller := &driveCoverageCaller{responses: map[string][]string{
|
||||
"get_file_info": {`{"success":true,"result":{"fileId":"uploaded-7","name":"input","extension":"bin","fileSize":18}}`},
|
||||
}}
|
||||
if err := runDriveCoverage(t, Upload, caller, "--file", "input.bin", "--yes"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
testseam.Swap(t, &driveDownload, func(_ context.Context, _ string, options localio.DownloadOptions) (localio.DownloadResult, error) {
|
||||
if options.Output != "downloads/file.bin" || options.Headers["x-token"] != "secret" {
|
||||
@@ -218,6 +253,42 @@ func TestCrossPlatformCoverageDriveCopyPreservesSchemaProperties(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDriveFirstInt64(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value any
|
||||
want int64
|
||||
ok bool
|
||||
}{
|
||||
{"int", int(1), 1, true},
|
||||
{"int32", int32(2), 2, true},
|
||||
{"int64", int64(3), 3, true},
|
||||
{"float", float64(4), 4, true},
|
||||
{"json number", json.Number("5"), 5, true},
|
||||
{"string", " 6 ", 6, true},
|
||||
{"fraction", 1.5, 0, false},
|
||||
{"nan", math.NaN(), 0, false},
|
||||
{"infinity", math.Inf(1), 0, false},
|
||||
{"overflow", float64(math.MaxInt64), 0, false},
|
||||
{"bad json number", json.Number("bad"), 0, false},
|
||||
{"bad string", "bad", 0, false},
|
||||
{"unsupported", true, 0, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, ok := firstInt64(map[string]any{"size": tc.value}, "missing", "size")
|
||||
if ok != tc.ok || got != tc.want {
|
||||
t.Fatalf("firstInt64(%#v) = (%d, %t), want (%d, %t)", tc.value, got, ok, tc.want, tc.ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
if got, ok := firstInt64(map[string]any{}, "size"); ok || got != 0 {
|
||||
t.Fatalf("missing firstInt64 = (%d, %t), want (0, false)", got, ok)
|
||||
}
|
||||
if got, ok := firstInt64(map[string]any{"fileSize": nil, "size": "7"}, "fileSize", "size"); !ok || got != 7 {
|
||||
t.Fatalf("fallback firstInt64 = (%d, %t), want (7, true)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDriveVersionAndPublishContracts(t *testing.T) {
|
||||
versionPayload := `{"success":true,"versions":[{"version":1,"fileSize":3},{"versionNumber":"2","fileSize":4}],"hasMore":false}`
|
||||
caller := &driveCoverageCaller{responses: map[string][]string{"list_file_versions": {versionPayload}}}
|
||||
@@ -449,11 +520,34 @@ func TestCrossPlatformCoverageDriveCreateRestoreCopyMoveRename(t *testing.T) {
|
||||
|
||||
move := &driveCoverageCaller{responses: map[string][]string{
|
||||
"move_document": {`{"success":true}`},
|
||||
"get_document_info": {`{"success":true,"result":{"nodeId":"n1"}}`},
|
||||
"get_document_info": {`{"success":true,"result":{"nodeId":"n1","folderId":"target","workspaceId":"space"}}`},
|
||||
}}
|
||||
if err := runDriveCoverage(t, Move, move, "--node", "n1", "--folder", "target", "--workspace", "space", "--yes"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
readback string
|
||||
want string
|
||||
}{
|
||||
{"missing node id", `{"success":true,"result":{"folderId":"target","workspaceId":"space"}}`, "缺少节点 ID"},
|
||||
{"wrong node id", `{"success":true,"result":{"nodeId":"other","folderId":"target","workspaceId":"space"}}`, "与请求节点 \"n1\" 不一致"},
|
||||
{"missing folder", `{"success":true,"result":{"nodeId":"n1","workspaceId":"space"}}`, "缺少目标文件夹 ID"},
|
||||
{"wrong folder", `{"success":true,"result":{"nodeId":"n1","folderId":"other","workspaceId":"space"}}`, "与请求 \"target\" 不一致"},
|
||||
{"missing workspace", `{"success":true,"result":{"nodeId":"n1","folderId":"target"}}`, "缺少目标知识库 ID"},
|
||||
{"wrong workspace", `{"success":true,"result":{"nodeId":"n1","folderId":"target","workspaceId":"other"}}`, "与请求 \"space\" 不一致"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
caller := &driveCoverageCaller{responses: map[string][]string{
|
||||
"move_document": {`{"success":true}`},
|
||||
"get_document_info": {tc.readback},
|
||||
}}
|
||||
err := runDriveCoverage(t, Move, caller, "--node", "n1", "--folder", "target", "--workspace", "space", "--yes")
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("error = %v, want %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
rename := &driveCoverageCaller{responses: map[string][]string{
|
||||
"get_file_info": {`{"success":true,"result":{"fileId":"n1","type":"FILE","extension":"md","name":"old.md"}}`, `{"success":true,"result":{"fileId":"n1","name":"new.md"}}`},
|
||||
|
||||
@@ -1313,6 +1313,8 @@ func validateRenamedSchemaParameter(
|
||||
oldParameter parameterSchema,
|
||||
newParameter parameterSchema,
|
||||
) error {
|
||||
// The migration authorizes only the CLI spelling change. Requiredness is
|
||||
// part of the parameter contract in both projections and must remain exact.
|
||||
if oldParameter.Type != newParameter.Type ||
|
||||
oldParameter.Property != newParameter.Property ||
|
||||
oldParameter.InterfaceType != newParameter.InterfaceType ||
|
||||
@@ -1328,17 +1330,17 @@ func validateRenamedSchemaParameter(
|
||||
migration.Canonical.Name,
|
||||
)
|
||||
}
|
||||
if oldParameter.Required && !newParameter.Required {
|
||||
if oldParameter.Required != newParameter.Required {
|
||||
return fmt.Errorf(
|
||||
"approved flag migration %q Schema parameter %q -> %q became optional",
|
||||
"approved flag migration %q Schema parameter %q -> %q changed requiredness",
|
||||
migration.Command,
|
||||
migration.Legacy.Name,
|
||||
migration.Canonical.Name,
|
||||
)
|
||||
}
|
||||
if oldParameter.CLIRequired && !newParameter.CLIRequired {
|
||||
if oldParameter.CLIRequired != newParameter.CLIRequired {
|
||||
return fmt.Errorf(
|
||||
"approved flag migration %q Schema parameter %q -> %q stopped being cli_required",
|
||||
"approved flag migration %q Schema parameter %q -> %q changed cli_required",
|
||||
migration.Command,
|
||||
migration.Legacy.Name,
|
||||
migration.Canonical.Name,
|
||||
|
||||
@@ -1218,8 +1218,11 @@ func TestCrossPlatformCoverageSchemaFlagMigrationNormalizesExactRename(t *testin
|
||||
t.Fatalf("normalized baseline retained legacy parameter %q", legacy)
|
||||
}
|
||||
}
|
||||
if canonical := tool.Parameters["conversation-id"]; !canonical.Required || !canonical.CLIRequired {
|
||||
t.Fatalf("canonical required transition was not normalized: %#v", canonical)
|
||||
if canonical := tool.Parameters["conversation-id"]; canonical.Required || canonical.CLIRequired {
|
||||
t.Fatalf("optional canonical rename changed requiredness: %#v", canonical)
|
||||
}
|
||||
if canonical := tool.Parameters["message-id"]; !canonical.Required || !canonical.CLIRequired {
|
||||
t.Fatalf("required canonical rename changed requiredness: %#v", canonical)
|
||||
}
|
||||
if tool.Constraints != current.Products["chat"].Tools["chat.edit_message"].Constraints {
|
||||
t.Fatalf("constraints were not normalized: %s", tool.Constraints)
|
||||
@@ -1269,6 +1272,39 @@ func TestCrossPlatformCoverageSchemaFlagMigrationRejectsSemanticDrift(t *testing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
want string
|
||||
mutate func(*parameterSchema)
|
||||
}{
|
||||
{name: "optional required promotion", want: "changed requiredness", mutate: func(parameter *parameterSchema) {
|
||||
parameter.Required = true
|
||||
}},
|
||||
{name: "optional cli_required promotion", want: "changed cli_required", mutate: func(parameter *parameterSchema) {
|
||||
parameter.CLIRequired = true
|
||||
}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
current := schemaFlagMigrationContract(true)
|
||||
product := current.Products["chat"]
|
||||
tool := product.Tools["chat.edit_message"]
|
||||
canonical := tool.Parameters["conversation-id"]
|
||||
test.mutate(&canonical)
|
||||
tool.Parameters["conversation-id"] = canonical
|
||||
product.Tools["chat.edit_message"] = tool
|
||||
current.Products["chat"] = product
|
||||
|
||||
_, err := normalizeSchemaFlagMigrations(
|
||||
schemaFlagMigrationContract(false),
|
||||
current,
|
||||
schemaFlagMigrationAuthorizations(),
|
||||
)
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("normalizeSchemaFlagMigrations() error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) {
|
||||
@@ -1321,9 +1357,10 @@ func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) {
|
||||
t.Fatal("missing candidate product was normalized away")
|
||||
}
|
||||
|
||||
canonicalOnly := schemaFlagMigrationAuthorizations()[0]
|
||||
canonicalOnly.Legacy.Name = "legacy-not-published-in-schema"
|
||||
driftedCanonical := cloneContract(current)
|
||||
canonicalOnlyMigration := schemaFlagMigrationAuthorizations()[0]
|
||||
canonicalOnlyMigration.Legacy.Name = "legacy-not-published-in-schema"
|
||||
canonicalOnlyBaseline := schemaFlagMigrationContract(true)
|
||||
driftedCanonical := cloneContract(canonicalOnlyBaseline)
|
||||
product = driftedCanonical.Products["chat"]
|
||||
tool = product.Tools["chat.edit_message"]
|
||||
canonical := tool.Parameters["conversation-id"]
|
||||
@@ -1331,7 +1368,7 @@ func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) {
|
||||
tool.Parameters["conversation-id"] = canonical
|
||||
product.Tools["chat.edit_message"] = tool
|
||||
driftedCanonical.Products["chat"] = product
|
||||
normalized, err = normalizeSchemaFlagMigrations(baseline, driftedCanonical, []interfacesnapshot.FlagMigration{canonicalOnly})
|
||||
normalized, err = normalizeSchemaFlagMigrations(canonicalOnlyBaseline, driftedCanonical, []interfacesnapshot.FlagMigration{canonicalOnlyMigration})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1339,24 +1376,24 @@ func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) {
|
||||
t.Fatalf("canonical-only Schema drift was hidden: %s", failures)
|
||||
}
|
||||
|
||||
canonicalOptional := schemaFlagMigrationContract(true)
|
||||
product = canonicalOptional.Products["chat"]
|
||||
promotedCanonical := cloneContract(canonicalOnlyBaseline)
|
||||
product = promotedCanonical.Products["chat"]
|
||||
tool = product.Tools["chat.edit_message"]
|
||||
canonical = tool.Parameters["conversation-id"]
|
||||
canonical.Required = false
|
||||
canonical.CLIRequired = false
|
||||
canonical.Required = true
|
||||
canonical.CLIRequired = true
|
||||
tool.Parameters["conversation-id"] = canonical
|
||||
product.Tools["chat.edit_message"] = tool
|
||||
canonicalOptional.Products["chat"] = product
|
||||
promotedCanonical.Products["chat"] = product
|
||||
normalized, err = normalizeSchemaFlagMigrations(
|
||||
canonicalOptional,
|
||||
schemaFlagMigrationContract(true),
|
||||
[]interfacesnapshot.FlagMigration{canonicalOnly},
|
||||
canonicalOnlyBaseline,
|
||||
promotedCanonical,
|
||||
[]interfacesnapshot.FlagMigration{canonicalOnlyMigration},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if failures := strings.Join(checkCompatibility(normalized, schemaFlagMigrationContract(true)), "\n"); !strings.Contains(failures, "newly required") || !strings.Contains(failures, "newly cli_required") {
|
||||
if failures := strings.Join(checkCompatibility(normalized, promotedCanonical), "\n"); !strings.Contains(failures, "newly required") || !strings.Contains(failures, "newly cli_required") {
|
||||
t.Fatalf("canonical-only required promotion was hidden: %s", failures)
|
||||
}
|
||||
|
||||
@@ -1392,12 +1429,19 @@ func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) {
|
||||
}
|
||||
|
||||
old := parameterSchema{Required: true, CLIRequired: true}
|
||||
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{CLIRequired: true}); err == nil || !strings.Contains(err.Error(), "became optional") {
|
||||
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{CLIRequired: true}); err == nil || !strings.Contains(err.Error(), "changed requiredness") {
|
||||
t.Fatalf("direct required decline error = %v", err)
|
||||
}
|
||||
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{Required: true}); err == nil || !strings.Contains(err.Error(), "stopped being cli_required") {
|
||||
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{Required: true}); err == nil || !strings.Contains(err.Error(), "changed cli_required") {
|
||||
t.Fatalf("direct cli_required decline error = %v", err)
|
||||
}
|
||||
optional := parameterSchema{}
|
||||
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], optional, parameterSchema{Required: true}); err == nil || !strings.Contains(err.Error(), "changed requiredness") {
|
||||
t.Fatalf("direct required promotion error = %v", err)
|
||||
}
|
||||
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], optional, parameterSchema{CLIRequired: true}); err == nil || !strings.Contains(err.Error(), "changed cli_required") {
|
||||
t.Fatalf("direct cli_required promotion error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageSchemaFlagMigrationRejectsPartialAndUnrelatedChanges(t *testing.T) {
|
||||
@@ -1471,9 +1515,8 @@ func TestCrossPlatformCoverageSchemaFlagMigrationRejectsPartialAndUnrelatedChang
|
||||
t.Fatalf("constraint rewrite without Schema parameter evidence was hidden: %s", failures)
|
||||
}
|
||||
|
||||
// A baseline that already contains only the canonical parameter may receive
|
||||
// a required promotion, but that is not evidence that a stray legacy name in
|
||||
// constraints belongs to the migration.
|
||||
// A baseline that already contains only the canonical parameter is not
|
||||
// evidence that a stray legacy name in constraints belongs to the migration.
|
||||
canonicalOnly := schemaFlagMigrationContract(true)
|
||||
product = canonicalOnly.Products["chat"]
|
||||
tool = product.Tools["chat.edit_message"]
|
||||
@@ -1749,13 +1792,10 @@ func schemaFlagMigrationContract(after bool) schemaContract {
|
||||
InterfaceType: "string",
|
||||
}
|
||||
parameters := map[string]parameterSchema{
|
||||
"conversation-id": conversation,
|
||||
"unrelated": {Type: `"string"`, Property: "unrelated"},
|
||||
"unrelated": {Type: `"string"`, Property: "unrelated"},
|
||||
}
|
||||
constraints := `{"require_one_of":[["conversation-id","group","id"]]}`
|
||||
constraints := `{"require_one_of":[["group","id"]]}`
|
||||
if after {
|
||||
conversation.Required = true
|
||||
conversation.CLIRequired = true
|
||||
parameters["conversation-id"] = conversation
|
||||
parameters["message-id"] = legacyMessage
|
||||
constraints = `{"require_one_of":[["conversation-id"]]}`
|
||||
@@ -1788,7 +1828,6 @@ func schemaFlagMigrationAuthorizations() []interfacesnapshot.FlagMigration {
|
||||
Scope: "local",
|
||||
}
|
||||
conversationAfter := conversationBefore
|
||||
conversationAfter.Required = true
|
||||
messageBefore := interfacesnapshot.FlagMigrationState{
|
||||
Present: true,
|
||||
Type: "string",
|
||||
@@ -1808,7 +1847,7 @@ func schemaFlagMigrationAuthorizations() []interfacesnapshot.FlagMigration {
|
||||
},
|
||||
Canonical: interfacesnapshot.FlagMigrationSide{
|
||||
Name: "conversation-id",
|
||||
Before: conversationBefore,
|
||||
Before: interfacesnapshot.FlagMigrationState{},
|
||||
After: conversationAfter,
|
||||
},
|
||||
},
|
||||
@@ -1823,7 +1862,7 @@ func schemaFlagMigrationAuthorizations() []interfacesnapshot.FlagMigration {
|
||||
},
|
||||
Canonical: interfacesnapshot.FlagMigrationSide{
|
||||
Name: "conversation-id",
|
||||
Before: conversationBefore,
|
||||
Before: interfacesnapshot.FlagMigrationState{},
|
||||
After: conversationAfter,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user