mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Per issue #1049 (third point), wiki +node-get used --token while sibling commands (+node-delete / +node-copy / +move) use --node-token. The inconsistency forced humans and AI agents to remember which adjacent command takes which flag. Make --node-token the canonical flag and keep --token as a hidden, deprecated alias so existing scripts continue to work. pflag's MarkDeprecated prints "Flag --token has been deprecated, use --node-token instead" to stderr on use, guiding callers to migrate. Conflict between the two with different values is rejected upfront. Skills docs (lark-wiki, lark-base) updated to prefer --node-token. Change-Id: I3415a98f079613c0b1a0b989cf54a09cbb8986fb
518 lines
16 KiB
Go
518 lines
16 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package wiki
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestParseWikiNodeGetSpecRawNodeToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
spec, err := parseWikiNodeGetSpec("wikcnABC", "", "")
|
|
if err != nil {
|
|
t.Fatalf("parseWikiNodeGetSpec() error = %v", err)
|
|
}
|
|
if spec.Token != "wikcnABC" || spec.ObjType != "" || spec.SourceKind != "raw-node" {
|
|
t.Fatalf("spec = %+v, want raw-node wikcnABC with no obj_type", spec)
|
|
}
|
|
if got := spec.RequestParams(); !reflect.DeepEqual(got, map[string]interface{}{"token": "wikcnABC"}) {
|
|
t.Fatalf("RequestParams() = %v, want {token: wikcnABC}", got)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRawObjTokenWithExplicitObjType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
spec, err := parseWikiNodeGetSpec("docxXYZ", "docx", "")
|
|
if err != nil {
|
|
t.Fatalf("parseWikiNodeGetSpec() error = %v", err)
|
|
}
|
|
if spec.Token != "docxXYZ" || spec.ObjType != "docx" || spec.SourceKind != "raw-obj" {
|
|
t.Fatalf("spec = %+v, want raw-obj docxXYZ obj_type=docx", spec)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRejectsRawObjTokenWithoutObjType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Mirrors +node-delete: a raw obj_token with no --obj-type must fail
|
|
// upfront instead of defaulting to "doc" and hitting an opaque API error.
|
|
_, err := parseWikiNodeGetSpec("bascnXYZ", "", "")
|
|
if err == nil || !strings.Contains(err.Error(), "--obj-type is required for a raw obj_token") {
|
|
t.Fatalf("expected raw obj_token obj-type-required error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRejectsObjTypeOnNodeToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseWikiNodeGetSpec("wikcnABC", "docx", "")
|
|
if err == nil || !strings.Contains(err.Error(), "only valid for obj_tokens") {
|
|
t.Fatalf("expected node_token + obj_type rejection, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecExtractsTokenFromWikiURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
spec, err := parseWikiNodeGetSpec("https://feishu.cn/wiki/wikcnABC?foo=bar", "", "")
|
|
if err != nil {
|
|
t.Fatalf("parseWikiNodeGetSpec() error = %v", err)
|
|
}
|
|
if spec.Token != "wikcnABC" || spec.ObjType != "" || spec.SourceKind != "url-wiki" {
|
|
t.Fatalf("spec = %+v, want url-wiki wikcnABC", spec)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecExtractsTokenAndObjTypeFromDocxURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
spec, err := parseWikiNodeGetSpec("https://feishu.cn/docx/docxXYZ", "", "")
|
|
if err != nil {
|
|
t.Fatalf("parseWikiNodeGetSpec() error = %v", err)
|
|
}
|
|
if spec.Token != "docxXYZ" || spec.ObjType != "docx" || spec.SourceKind != "url-obj" {
|
|
t.Fatalf("spec = %+v, want url-obj docxXYZ", spec)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRejectsURLObjTypeMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseWikiNodeGetSpec("https://feishu.cn/sheets/shtXYZ", "docx", "")
|
|
if err == nil || !strings.Contains(err.Error(), "does not match the obj_type") {
|
|
t.Fatalf("expected URL/obj-type mismatch error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRejectsUnsupportedURLPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseWikiNodeGetSpec("https://feishu.cn/im/chat/oc_123", "", "")
|
|
if err == nil || !strings.Contains(err.Error(), "unsupported --node-token URL path") {
|
|
t.Fatalf("expected unsupported URL path error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRejectsPartialPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseWikiNodeGetSpec("/wiki/wikcnABC", "", "")
|
|
if err == nil || !strings.Contains(err.Error(), "partial paths are not accepted") {
|
|
t.Fatalf("expected partial-path rejection, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseWikiNodeGetSpecRejectsEmptyToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if _, err := parseWikiNodeGetSpec(" ", "", ""); err == nil || !strings.Contains(err.Error(), "--node-token is required") {
|
|
t.Fatalf("expected required-token error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveWikiNodeGetRawTokenPrefersNodeToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := resolveWikiNodeGetRawToken("wikcnNEW", "")
|
|
if err != nil || got != "wikcnNEW" {
|
|
t.Fatalf("resolve(node-token only) = (%q, %v), want (wikcnNEW, nil)", got, err)
|
|
}
|
|
}
|
|
|
|
func TestResolveWikiNodeGetRawTokenAcceptsLegacyToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, err := resolveWikiNodeGetRawToken("", "wikcnLEGACY")
|
|
if err != nil || got != "wikcnLEGACY" {
|
|
t.Fatalf("resolve(legacy only) = (%q, %v), want (wikcnLEGACY, nil)", got, err)
|
|
}
|
|
}
|
|
|
|
func TestResolveWikiNodeGetRawTokenAcceptsBothWhenEqual(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Same value on both flags is harmless (e.g. a script doubled the input
|
|
// while migrating to --node-token) — prefer the canonical one and don't
|
|
// surface a conflict error.
|
|
got, err := resolveWikiNodeGetRawToken("wikcnSAME", "wikcnSAME")
|
|
if err != nil || got != "wikcnSAME" {
|
|
t.Fatalf("resolve(both same) = (%q, %v), want (wikcnSAME, nil)", got, err)
|
|
}
|
|
}
|
|
|
|
func TestResolveWikiNodeGetRawTokenRejectsConflict(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := resolveWikiNodeGetRawToken("wikcnNEW", "wikcnOLD")
|
|
if err == nil || !strings.Contains(err.Error(), "both set with different values") {
|
|
t.Fatalf("expected conflict error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveWikiNodeGetRawTokenEmptyDefersToParser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Both empty is not an error here — the caller (parseWikiNodeGetSpec) is
|
|
// where the required-flag check lives and produces the user-facing message.
|
|
got, err := resolveWikiNodeGetRawToken("", "")
|
|
if err != nil || got != "" {
|
|
t.Fatalf("resolve(empty) = (%q, %v), want ('', nil)", got, err)
|
|
}
|
|
}
|
|
|
|
func TestBuildWikiNodeGetDryRunSendsObjType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
spec, err := parseWikiNodeGetSpec("https://feishu.cn/docx/docxXYZ", "", "")
|
|
if err != nil {
|
|
t.Fatalf("parseWikiNodeGetSpec() error = %v", err)
|
|
}
|
|
|
|
dry := buildWikiNodeGetDryRun(spec)
|
|
data, err := json.Marshal(dry)
|
|
if err != nil {
|
|
t.Fatalf("marshal dry run: %v", err)
|
|
}
|
|
var got struct {
|
|
API []struct {
|
|
Method string `json:"method"`
|
|
URL string `json:"url"`
|
|
Params map[string]interface{} `json:"params"`
|
|
} `json:"api"`
|
|
}
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatalf("unmarshal dry run: %v", err)
|
|
}
|
|
if len(got.API) != 1 || got.API[0].URL != "/open-apis/wiki/v2/spaces/get_node" {
|
|
t.Fatalf("dry-run api = %#v, want single get_node call", got.API)
|
|
}
|
|
if got.API[0].Params["token"] != "docxXYZ" || got.API[0].Params["obj_type"] != "docx" {
|
|
t.Fatalf("dry-run params = %#v", got.API[0].Params)
|
|
}
|
|
}
|
|
|
|
func TestFormatWikiTimestamp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := formatWikiTimestamp(""); got != "" {
|
|
t.Fatalf("formatWikiTimestamp(empty) = %q, want empty", got)
|
|
}
|
|
if got := formatWikiTimestamp("not-a-number"); got != "" {
|
|
t.Fatalf("formatWikiTimestamp(non-numeric) = %q, want empty", got)
|
|
}
|
|
// Output is UTC, so it is deterministic regardless of host timezone.
|
|
if got := formatWikiTimestamp("1700000000"); got != "2023-11-14T22:13:20Z" {
|
|
t.Fatalf("formatWikiTimestamp(1700000000) = %q, want 2023-11-14T22:13:20Z (UTC)", got)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeGetMountedExecuteParsesURLAndFormatsOutput(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, stderr, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
stub := &httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/get_node",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_123",
|
|
"node_token": "wikcnABC",
|
|
"obj_token": "docxXYZ",
|
|
"obj_type": "docx",
|
|
"parent_node_token": "wikcnPARENT",
|
|
"node_type": "origin",
|
|
"title": "Design Spec",
|
|
"has_child": true,
|
|
"node_creator": "ou_creator",
|
|
"owner": "ou_owner",
|
|
"obj_edit_time": "1700000000",
|
|
"obj_create_time": "1690000000",
|
|
"node_create_time": "1690000001",
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
}
|
|
var capturedQuery string
|
|
stub.OnMatch = func(req *http.Request) {
|
|
capturedQuery = req.URL.RawQuery
|
|
}
|
|
reg.Register(stub)
|
|
|
|
err := mountAndRunWiki(t, WikiNodeGet, []string{
|
|
"+node-get",
|
|
"--node-token", "https://feishu.cn/docx/docxXYZ",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
if !strings.Contains(capturedQuery, "token=docxXYZ") || !strings.Contains(capturedQuery, "obj_type=docx") {
|
|
t.Fatalf("captured query = %q, want token=docxXYZ and obj_type=docx", capturedQuery)
|
|
}
|
|
|
|
data := decodeWikiEnvelope(t, stdout)
|
|
if data["title"] != "Design Spec" {
|
|
t.Fatalf("title = %#v, want Design Spec", data["title"])
|
|
}
|
|
if data["obj_type"] != "docx" || data["obj_token"] != "docxXYZ" {
|
|
t.Fatalf("obj_type/obj_token = %#v / %#v", data["obj_type"], data["obj_token"])
|
|
}
|
|
if data["parent_node_token"] != "wikcnPARENT" {
|
|
t.Fatalf("parent_node_token = %#v", data["parent_node_token"])
|
|
}
|
|
if data["creator"] != "ou_creator" {
|
|
t.Fatalf("creator = %#v, want ou_creator", data["creator"])
|
|
}
|
|
if data["owner"] != "ou_owner" {
|
|
t.Fatalf("owner = %#v, want ou_owner", data["owner"])
|
|
}
|
|
if got, _ := data["updated_at"].(string); got != "2023-11-14T22:13:20Z" {
|
|
t.Fatalf("updated_at = %#v, want 2023-11-14T22:13:20Z (UTC)", data["updated_at"])
|
|
}
|
|
// +node-get deliberately does not synthesize a url (get_node returns none;
|
|
// a BuildResourceURL fallback would be a non-canonical, misleading link in
|
|
// a read/confirm command).
|
|
if _, ok := data["url"]; ok {
|
|
t.Fatalf("did not expect a url field in +node-get output, got %#v", data["url"])
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "Fetching wiki node") {
|
|
t.Fatalf("stderr = %q, want fetching message", got)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeGetMountedAcceptsNodeTokenFlag(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
stub := &httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/get_node",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_123",
|
|
"node_token": "wikcnABC",
|
|
"obj_token": "docxXYZ",
|
|
"obj_type": "docx",
|
|
"node_type": "origin",
|
|
"title": "Via Node-Token",
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
}
|
|
var capturedQuery string
|
|
stub.OnMatch = func(req *http.Request) {
|
|
capturedQuery = req.URL.RawQuery
|
|
}
|
|
reg.Register(stub)
|
|
|
|
// Mount inline (rather than using mountAndRunWiki) so we can redirect the
|
|
// subcommand's pflag output and assert that no deprecation warning leaks
|
|
// when the canonical --node-token is used. The deprecation message comes
|
|
// from pflag, not cobra, so SetErr on the cobra root is NOT enough — pflag
|
|
// writes to FlagSet.Output(), which we redirect via Flags().SetOutput.
|
|
var flagOut bytes.Buffer
|
|
parent := mountWikiNodeGetWithFlagOut(t, factory, &flagOut)
|
|
parent.SetArgs([]string{
|
|
"+node-get",
|
|
"--node-token", "https://feishu.cn/docx/docxXYZ",
|
|
"--as", "bot",
|
|
})
|
|
stdout.Reset()
|
|
if err := parent.Execute(); err != nil {
|
|
t.Fatalf("parent.Execute() error = %v", err)
|
|
}
|
|
|
|
if !strings.Contains(capturedQuery, "token=docxXYZ") || !strings.Contains(capturedQuery, "obj_type=docx") {
|
|
t.Fatalf("captured query = %q, want token=docxXYZ and obj_type=docx", capturedQuery)
|
|
}
|
|
|
|
data := decodeWikiEnvelope(t, stdout)
|
|
if data["title"] != "Via Node-Token" {
|
|
t.Fatalf("title = %#v, want Via Node-Token", data["title"])
|
|
}
|
|
if got := flagOut.String(); strings.Contains(got, "deprecated") {
|
|
t.Fatalf("pflag output unexpectedly contains deprecation warning when using --node-token: %q", got)
|
|
}
|
|
}
|
|
|
|
// mountWikiNodeGetWithFlagOut mounts +node-get on a fresh parent and redirects
|
|
// the subcommand's pflag output to w so tests can capture cobra/pflag-level
|
|
// deprecation messages (which bypass the runtime IO stderr exposed by
|
|
// TestFactory).
|
|
func mountWikiNodeGetWithFlagOut(t *testing.T, factory *cmdutil.Factory, w *bytes.Buffer) *cobra.Command {
|
|
t.Helper()
|
|
parent := &cobra.Command{Use: "wiki"}
|
|
WikiNodeGet.Mount(parent, factory)
|
|
parent.SilenceErrors = true
|
|
parent.SilenceUsage = true
|
|
parent.SetErr(w)
|
|
for _, child := range parent.Commands() {
|
|
if child.Use == WikiNodeGet.Command {
|
|
child.Flags().SetOutput(w)
|
|
return parent
|
|
}
|
|
}
|
|
t.Fatalf("mountWikiNodeGetWithFlagOut: subcommand %q not registered on parent", WikiNodeGet.Command)
|
|
return nil
|
|
}
|
|
|
|
func TestWikiNodeGetMountedLegacyTokenFlagWarnsButWorks(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/get_node",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_123",
|
|
"node_token": "wikcnABC",
|
|
"obj_token": "docxXYZ",
|
|
"obj_type": "docx",
|
|
"node_type": "origin",
|
|
"title": "Legacy Token Path",
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
})
|
|
|
|
var flagOut bytes.Buffer
|
|
parent := mountWikiNodeGetWithFlagOut(t, factory, &flagOut)
|
|
parent.SetArgs([]string{
|
|
"+node-get",
|
|
"--token", "wikcnABC",
|
|
"--as", "bot",
|
|
})
|
|
stdout.Reset()
|
|
if err := parent.Execute(); err != nil {
|
|
t.Fatalf("parent.Execute() error = %v", err)
|
|
}
|
|
|
|
data := decodeWikiEnvelope(t, stdout)
|
|
if data["title"] != "Legacy Token Path" {
|
|
t.Fatalf("title = %#v, want Legacy Token Path", data["title"])
|
|
}
|
|
// pflag MarkDeprecated prints "Flag --token has been deprecated, use --node-token instead".
|
|
got := flagOut.String()
|
|
if !strings.Contains(got, "deprecated") || !strings.Contains(got, "--node-token") {
|
|
t.Fatalf("pflag output = %q, want a deprecation warning pointing to --node-token", got)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeGetMountedRejectsConflictingTokenFlags(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
// reg is unused: conflict is caught in Validate before any HTTP call.
|
|
factory, stdout, _, _ := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
err := mountAndRunWiki(t, WikiNodeGet, []string{
|
|
"+node-get",
|
|
"--node-token", "wikcnNEW",
|
|
"--token", "wikcnOLD",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "both set with different values") {
|
|
t.Fatalf("expected conflict error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeGetFallsBackToCreatorWhenNodeCreatorMissing(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/get_node",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_123",
|
|
"node_token": "wikcnABC",
|
|
"obj_token": "docxXYZ",
|
|
"obj_type": "docx",
|
|
"node_type": "origin",
|
|
"title": "Fallback Creator",
|
|
"creator": "ou_legacy_creator",
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeGet, []string{
|
|
"+node-get",
|
|
"--node-token", "wikcnABC",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err != nil {
|
|
t.Fatalf("mountAndRunWiki() error = %v", err)
|
|
}
|
|
|
|
data := decodeWikiEnvelope(t, stdout)
|
|
if data["creator"] != "ou_legacy_creator" {
|
|
t.Fatalf("creator = %#v, want fallback to creator field", data["creator"])
|
|
}
|
|
}
|
|
|
|
func TestWikiNodeGetRejectsSpaceIDMismatch(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
|
|
|
factory, stdout, _, reg := cmdutil.TestFactory(t, wikiTestConfig())
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/wiki/v2/spaces/get_node",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"data": map[string]interface{}{
|
|
"node": map[string]interface{}{
|
|
"space_id": "space_actual",
|
|
"node_token": "wikcnABC",
|
|
"obj_token": "docxXYZ",
|
|
"obj_type": "docx",
|
|
"node_type": "origin",
|
|
"title": "Mismatch",
|
|
},
|
|
},
|
|
"msg": "success",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunWiki(t, WikiNodeGet, []string{
|
|
"+node-get",
|
|
"--node-token", "wikcnABC",
|
|
"--space-id", "space_expected",
|
|
"--as", "bot",
|
|
}, factory, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "does not match the resolved node space") {
|
|
t.Fatalf("expected space mismatch error, got %v", err)
|
|
}
|
|
}
|