draftgo-cli 4.0.1 → 4.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -11
- package/package.json +9 -4
- package/resources/custom-service-sdk/ai.go +520 -0
- package/resources/custom-service-sdk/ai_test.go +156 -0
- package/resources/custom-service-sdk/auth_test.go +56 -0
- package/resources/custom-service-sdk/billing.go +596 -0
- package/resources/custom-service-sdk/billing_test.go +150 -0
- package/resources/custom-service-sdk/go.mod +3 -0
- package/resources/custom-service-sdk/manifest.json +77 -0
- package/resources/custom-service-sdk/platform.go +345 -0
- package/resources/custom-service-sdk/platform_logger_test.go +24 -0
- package/resources/custom-service-sdk/registration_test.go +39 -0
- package/resources/custom-service-sdk/resources.go +246 -0
- package/resources/custom-service-sdk/resources_billing_test.go +115 -0
- package/resources/custom-service-sdk/resources_files_test.go +57 -0
- package/resources/custom-service-sdk/resources_scope_test.go +87 -0
- package/resources/custom-service-sdk/sdk.go +208 -0
- package/resources/skill/SKILL.md +36 -88
- package/resources/skill/init/SKILL.md +4 -4
- package/resources/skill/manifest.json +5 -1
- package/resources/skill/references/aihub.md +25 -2
- package/resources/skill/references/app-api.md +56 -6
- package/resources/skill/references/architecture.md +2 -2
- package/resources/skill/references/chat-sdk.md +4 -2
- package/resources/skill/references/checkout.md +17 -3
- package/resources/skill/references/custom-services.md +112 -47
- package/resources/skill/references/data.md +19 -4
- package/resources/skill/references/delivery.md +33 -0
- package/resources/skill/references/diagnostics.md +51 -0
- package/resources/skill/references/frontend.md +34 -46
- package/resources/skill/references/mcp.md +33 -5
- package/resources/skill/references/methods.md +189 -0
- package/resources/skill/references/modules.md +37 -9
- package/resources/skill/references/runtime.md +23 -1
- package/src/cli.js +24 -0
- package/src/commandRegistry.js +9 -1
- package/src/commands/api.js +21 -10
- package/src/commands/apiKey.js +34 -0
- package/src/commands/capabilities.js +93 -0
- package/src/commands/checkout.js +1 -1
- package/src/commands/commit.js +1 -1
- package/src/commands/components.js +550 -0
- package/src/commands/conflict.js +1 -1
- package/src/commands/connect.js +18 -8
- package/src/commands/customService.js +20 -4
- package/src/commands/dataRange.js +33 -0
- package/src/commands/delete.js +12 -1
- package/src/commands/diff.js +18 -2
- package/src/commands/grant.js +29 -0
- package/src/commands/group.js +38 -0
- package/src/commands/help.js +64 -20
- package/src/commands/init.js +3 -3
- package/src/commands/map.js +145 -17
- package/src/commands/mcp.js +2 -2
- package/src/commands/reconcile.js +1 -1
- package/src/commands/role.js +32 -0
- package/src/commands/space.js +41 -0
- package/src/commands/status.js +110 -7
- package/src/commands/update.js +23 -11
- package/src/commands/verify.js +75 -0
- package/src/commands/worklog.js +6 -2
- package/src/consoleEncoding.js +34 -0
- package/src/contractCompatibility.js +57 -0
- package/src/customServices.js +138 -18
- package/src/diffReport.js +106 -0
- package/src/index.js +2 -0
- package/src/localRuntime/compose.js +14 -17
- package/src/localRuntime/index.js +22 -23
- package/src/localRuntime/services.js +27 -36
- package/src/mcp/client.js +11 -2
- package/src/mcp/protocol.js +22 -2
- package/src/mcp/tools.js +14 -1
- package/src/platforms.js +9 -0
- package/src/projectConfig.js +6 -4
- package/src/releaseInstall.js +105 -0
- package/src/updateCheck.js +48 -28
- package/src/worklog.js +2 -1
- package/src/worktree/backend.js +1 -1
- package/src/worktree/index.js +7 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
package sdk
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/base64"
|
|
6
|
+
"fmt"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const (
|
|
10
|
+
ScopePlatform = "platform"
|
|
11
|
+
ScopeSpace = "space"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// FileStore is the generic file-management facade. File bytes are exchanged
|
|
15
|
+
// through the host; custom services never receive object-store paths.
|
|
16
|
+
type FileStore interface {
|
|
17
|
+
ListFolders(context.Context, map[string]any) (map[string]any, error)
|
|
18
|
+
GetFolder(context.Context, int64) (map[string]any, error)
|
|
19
|
+
CreateFolder(context.Context, FileFolderInput) (map[string]any, error)
|
|
20
|
+
UpdateFolder(context.Context, int64, FileFolderUpdateInput) (map[string]any, error)
|
|
21
|
+
DeleteFolder(context.Context, int64, bool) error
|
|
22
|
+
RestoreFolder(context.Context, int64) (map[string]any, error)
|
|
23
|
+
ListAssets(context.Context, map[string]any) (map[string]any, error)
|
|
24
|
+
GetAsset(context.Context, int64) (map[string]any, error)
|
|
25
|
+
Upload(context.Context, FileUploadRequest) (map[string]any, error)
|
|
26
|
+
UpdateAsset(context.Context, int64, map[string]any) (map[string]any, error)
|
|
27
|
+
Move(context.Context, int64, int64) (map[string]any, error)
|
|
28
|
+
Trash(context.Context, int64) error
|
|
29
|
+
Restore(context.Context, int64) (map[string]any, error)
|
|
30
|
+
Purge(context.Context, int64) error
|
|
31
|
+
Download(context.Context, int64) (FileDownload, error)
|
|
32
|
+
ListBindings(context.Context, int64, bool) (map[string]any, error)
|
|
33
|
+
Unbind(context.Context, FileBindingRequest) error
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type FileFolderInput struct {
|
|
37
|
+
Name string `json:"name"`
|
|
38
|
+
ParentID int64 `json:"parent_id,omitempty"`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// FileFolderUpdateInput uses pointers so moving a folder to the root
|
|
42
|
+
// (parent_id=0) is distinguishable from leaving its parent unchanged.
|
|
43
|
+
type FileFolderUpdateInput struct {
|
|
44
|
+
Name *string `json:"name,omitempty"`
|
|
45
|
+
ParentID *int64 `json:"parent_id,omitempty"`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type FileUploadRequest struct {
|
|
49
|
+
Filename string `json:"filename"`
|
|
50
|
+
MIMEType string `json:"mime_type,omitempty"`
|
|
51
|
+
Content []byte `json:"-"`
|
|
52
|
+
FolderID int64 `json:"folder_id,omitempty"`
|
|
53
|
+
Visibility string `json:"visibility,omitempty"`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type FileDownload struct {
|
|
57
|
+
Filename string
|
|
58
|
+
MIMEType string
|
|
59
|
+
Content []byte
|
|
60
|
+
URL string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type FileBindingRequest struct {
|
|
64
|
+
AssetID int64 `json:"asset_id"`
|
|
65
|
+
BindingType string `json:"binding_type"`
|
|
66
|
+
BindingID int64 `json:"binding_id"`
|
|
67
|
+
Relation string `json:"relation,omitempty"`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ScopeAPI exposes invocation identity and permission checks. Current returns
|
|
71
|
+
// the verified ResourceContext so callers do not confuse persisted ownership
|
|
72
|
+
// with a request-global workspace selection.
|
|
73
|
+
type ScopeAPI interface {
|
|
74
|
+
CurrentPrincipal(context.Context) (ScopePrincipal, error)
|
|
75
|
+
Current(context.Context) (ResourceContext, error)
|
|
76
|
+
Check(context.Context, string, ResourceOwnership) error
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var (
|
|
80
|
+
_ FileStore = fileStoreClient{}
|
|
81
|
+
_ ScopeAPI = scopeClient{}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
type ScopePrincipal struct {
|
|
85
|
+
SubjectType string `json:"subject_type"`
|
|
86
|
+
SubjectID string `json:"subject_id"`
|
|
87
|
+
Username string `json:"username,omitempty"`
|
|
88
|
+
IsAdmin bool `json:"is_admin,omitempty"`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ResourceContext is the persisted ownership context inherited by the custom
|
|
92
|
+
// service invocation. It is not a selected workspace or request-global scope.
|
|
93
|
+
type ResourceContext struct {
|
|
94
|
+
ScopeType string `json:"scope_type"`
|
|
95
|
+
WorkspaceID int64 `json:"workspace_id,omitempty"`
|
|
96
|
+
SpaceID int64 `json:"space_id,omitempty"`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ResourceOwnership identifies the persisted authorization boundary of a
|
|
100
|
+
// resource. OwnerUserID is used only for permissions with own/all data range.
|
|
101
|
+
type ResourceOwnership struct {
|
|
102
|
+
ScopeType string `json:"scope_type"`
|
|
103
|
+
WorkspaceID int64 `json:"workspace_id,omitempty"`
|
|
104
|
+
SpaceID int64 `json:"space_id,omitempty"`
|
|
105
|
+
OwnerUserID int64 `json:"owner_user_id,omitempty"`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func resourceOwnership(value map[string]any) ResourceOwnership {
|
|
109
|
+
return ResourceOwnership{
|
|
110
|
+
ScopeType: fmtString(value["ownership_type"]),
|
|
111
|
+
WorkspaceID: asInt64(value["workspace_id"]),
|
|
112
|
+
SpaceID: asInt64(value["space_id"]),
|
|
113
|
+
OwnerUserID: asInt64(value["owner_user_id"]),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func (p platformClient) resourceRequest(ctx context.Context, method, path string, query, body any) (map[string]any, error) {
|
|
118
|
+
value, err := p.call(ctx, "resource.request", map[string]any{"method": method, "path": path, "query": query, "body": body})
|
|
119
|
+
return mapResult(value), err
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type fileStoreClient struct{ platformClient }
|
|
123
|
+
type scopeClient struct{ platformClient }
|
|
124
|
+
|
|
125
|
+
func (p fileStoreClient) request(ctx context.Context, operation string, args any) (map[string]any, error) {
|
|
126
|
+
value, err := p.call(ctx, operation, args)
|
|
127
|
+
return mapResult(value), err
|
|
128
|
+
}
|
|
129
|
+
func (p fileStoreClient) ListFolders(ctx context.Context, query map[string]any) (map[string]any, error) {
|
|
130
|
+
return p.request(ctx, "files.folder.list", map[string]any{"query": query})
|
|
131
|
+
}
|
|
132
|
+
func (p fileStoreClient) GetFolder(ctx context.Context, id int64) (map[string]any, error) {
|
|
133
|
+
return p.request(ctx, "files.folder.get", map[string]any{"id": id})
|
|
134
|
+
}
|
|
135
|
+
func (p fileStoreClient) CreateFolder(ctx context.Context, input FileFolderInput) (map[string]any, error) {
|
|
136
|
+
return p.request(ctx, "files.folder.create", input)
|
|
137
|
+
}
|
|
138
|
+
func (p fileStoreClient) UpdateFolder(ctx context.Context, id int64, input FileFolderUpdateInput) (map[string]any, error) {
|
|
139
|
+
return p.request(ctx, "files.folder.update", map[string]any{"id": id, "data": input})
|
|
140
|
+
}
|
|
141
|
+
func (p fileStoreClient) DeleteFolder(ctx context.Context, id int64, force bool) error {
|
|
142
|
+
_, err := p.request(ctx, "files.folder.delete", map[string]any{"id": id, "force": force})
|
|
143
|
+
return err
|
|
144
|
+
}
|
|
145
|
+
func (p fileStoreClient) RestoreFolder(ctx context.Context, id int64) (map[string]any, error) {
|
|
146
|
+
return p.request(ctx, "files.folder.restore", map[string]any{"id": id})
|
|
147
|
+
}
|
|
148
|
+
func (p fileStoreClient) ListAssets(ctx context.Context, query map[string]any) (map[string]any, error) {
|
|
149
|
+
return p.request(ctx, "files.asset.list", map[string]any{"query": query})
|
|
150
|
+
}
|
|
151
|
+
func (p fileStoreClient) GetAsset(ctx context.Context, id int64) (map[string]any, error) {
|
|
152
|
+
return p.request(ctx, "files.asset.get", map[string]any{"id": id})
|
|
153
|
+
}
|
|
154
|
+
func (p fileStoreClient) Upload(ctx context.Context, input FileUploadRequest) (map[string]any, error) {
|
|
155
|
+
return p.request(ctx, "files.asset.upload", map[string]any{"filename": input.Filename, "mime_type": input.MIMEType, "content_base64": base64.StdEncoding.EncodeToString(input.Content), "folder_id": input.FolderID, "visibility": input.Visibility})
|
|
156
|
+
}
|
|
157
|
+
func (p fileStoreClient) UpdateAsset(ctx context.Context, id int64, data map[string]any) (map[string]any, error) {
|
|
158
|
+
return p.request(ctx, "files.asset.update", map[string]any{"id": id, "data": data})
|
|
159
|
+
}
|
|
160
|
+
func (p fileStoreClient) Move(ctx context.Context, id, folderID int64) (map[string]any, error) {
|
|
161
|
+
return p.request(ctx, "files.asset.move", map[string]any{"id": id, "folder_id": folderID})
|
|
162
|
+
}
|
|
163
|
+
func (p fileStoreClient) Trash(ctx context.Context, id int64) error {
|
|
164
|
+
_, err := p.request(ctx, "files.asset.trash", map[string]any{"id": id})
|
|
165
|
+
return err
|
|
166
|
+
}
|
|
167
|
+
func (p fileStoreClient) Restore(ctx context.Context, id int64) (map[string]any, error) {
|
|
168
|
+
return p.request(ctx, "files.asset.restore", map[string]any{"id": id})
|
|
169
|
+
}
|
|
170
|
+
func (p fileStoreClient) Purge(ctx context.Context, id int64) error {
|
|
171
|
+
_, err := p.request(ctx, "files.asset.purge", map[string]any{"id": id})
|
|
172
|
+
return err
|
|
173
|
+
}
|
|
174
|
+
func (p fileStoreClient) Download(ctx context.Context, id int64) (FileDownload, error) {
|
|
175
|
+
value, err := p.call(ctx, "files.asset.download", map[string]any{"id": id})
|
|
176
|
+
if err != nil {
|
|
177
|
+
return FileDownload{}, err
|
|
178
|
+
}
|
|
179
|
+
object := mapResult(value)
|
|
180
|
+
content, decodeErr := decodeBase64Field(object["content_base64"])
|
|
181
|
+
if decodeErr != nil {
|
|
182
|
+
return FileDownload{}, decodeErr
|
|
183
|
+
}
|
|
184
|
+
return FileDownload{Filename: fmtString(object["filename"]), MIMEType: fmtString(object["mime_type"]), URL: fmtString(object["url"]), Content: content}, nil
|
|
185
|
+
}
|
|
186
|
+
func (p fileStoreClient) ListBindings(ctx context.Context, assetID int64, includeReleased bool) (map[string]any, error) {
|
|
187
|
+
return p.request(ctx, "files.binding.list", map[string]any{"asset_id": assetID, "include_released": includeReleased})
|
|
188
|
+
}
|
|
189
|
+
func (p fileStoreClient) Unbind(ctx context.Context, input FileBindingRequest) error {
|
|
190
|
+
_, err := p.request(ctx, "files.binding.release", input)
|
|
191
|
+
return err
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func (p scopeClient) request(ctx context.Context, operation string, args any) (map[string]any, error) {
|
|
195
|
+
value, err := p.call(ctx, operation, args)
|
|
196
|
+
return mapResult(value), err
|
|
197
|
+
}
|
|
198
|
+
func (p scopeClient) Current(ctx context.Context) (ResourceContext, error) {
|
|
199
|
+
value, err := p.request(ctx, "resource.context.current", nil)
|
|
200
|
+
if err != nil {
|
|
201
|
+
return ResourceContext{}, err
|
|
202
|
+
}
|
|
203
|
+
return ResourceContext{
|
|
204
|
+
ScopeType: fmtString(value["scope_type"]),
|
|
205
|
+
WorkspaceID: asInt64(value["workspace_id"]),
|
|
206
|
+
SpaceID: asInt64(value["space_id"]),
|
|
207
|
+
}, nil
|
|
208
|
+
}
|
|
209
|
+
func (p scopeClient) CurrentPrincipal(ctx context.Context) (ScopePrincipal, error) {
|
|
210
|
+
value, err := p.request(ctx, "scope.principal.current", nil)
|
|
211
|
+
if err != nil {
|
|
212
|
+
return ScopePrincipal{}, err
|
|
213
|
+
}
|
|
214
|
+
return ScopePrincipal{
|
|
215
|
+
SubjectType: fmtString(value["subject_type"]),
|
|
216
|
+
SubjectID: fmtString(value["subject_id"]),
|
|
217
|
+
Username: fmtString(value["username"]),
|
|
218
|
+
IsAdmin: asBool(value["is_admin"]),
|
|
219
|
+
}, nil
|
|
220
|
+
}
|
|
221
|
+
func (p scopeClient) Check(ctx context.Context, permission string, resource ResourceOwnership) error {
|
|
222
|
+
_, err := p.request(ctx, "scope.permission.check", map[string]any{"permission": permission, "resource": resource})
|
|
223
|
+
return err
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
func fmtString(value any) string {
|
|
227
|
+
if value == nil {
|
|
228
|
+
return ""
|
|
229
|
+
}
|
|
230
|
+
return fmt.Sprint(value)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func decodeBase64Field(value any) ([]byte, error) {
|
|
234
|
+
switch typed := value.(type) {
|
|
235
|
+
case []byte:
|
|
236
|
+
return typed, nil
|
|
237
|
+
case string:
|
|
238
|
+
decoded, err := base64.StdEncoding.DecodeString(typed)
|
|
239
|
+
if err != nil {
|
|
240
|
+
return nil, fmt.Errorf("decode file content: %w", err)
|
|
241
|
+
}
|
|
242
|
+
return decoded, nil
|
|
243
|
+
default:
|
|
244
|
+
return nil, nil
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
package sdk
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"net/http"
|
|
6
|
+
"reflect"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type contractClient struct {
|
|
11
|
+
operations []string
|
|
12
|
+
args []any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
func (c *contractClient) Call(_ context.Context, operation string, args any) (any, error) {
|
|
16
|
+
c.operations = append(c.operations, operation)
|
|
17
|
+
c.args = append(c.args, args)
|
|
18
|
+
switch operation {
|
|
19
|
+
case "billing.payment.create":
|
|
20
|
+
return map[string]any{"order_id": "po_1", "provider": "wechat", "status": "pending", "amount_minor": float64(123), "pay_url": "https://pay"}, nil
|
|
21
|
+
case "billing.ai.session.start", "billing.ai.session.settle":
|
|
22
|
+
return map[string]any{"id": "bs_1", "status": "reserved", "reserved_minor": float64(10), "settled_minor": float64(0), "price_version": "v1"}, nil
|
|
23
|
+
case "files.asset.download":
|
|
24
|
+
return map[string]any{"filename": "a.txt", "mime_type": "text/plain", "content_base64": "aGVsbG8="}, nil
|
|
25
|
+
case "resource.context.current":
|
|
26
|
+
return map[string]any{"scope_type": "space", "workspace_id": float64(2), "space_id": float64(8)}, nil
|
|
27
|
+
default:
|
|
28
|
+
return map[string]any{"ok": true}, nil
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func TestGenericResourceAndBillingFacade(t *testing.T) {
|
|
33
|
+
client := &contractClient{}
|
|
34
|
+
ctx := NewContext(context.Background(), nil, map[string]any{"id": float64(3)}, http.Header{}, client)
|
|
35
|
+
|
|
36
|
+
if _, err := ctx.Files.Upload(ctx.Context(), FileUploadRequest{Filename: "a.txt", Content: []byte("hello")}); err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
download, err := ctx.Files.Download(ctx.Context(), 1)
|
|
40
|
+
if err != nil || string(download.Content) != "hello" || download.Filename != "a.txt" {
|
|
41
|
+
t.Fatalf("download = %#v, err = %v", download, err)
|
|
42
|
+
}
|
|
43
|
+
if _, err = ctx.Scope.Current(ctx.Context()); err != nil {
|
|
44
|
+
t.Fatal(err)
|
|
45
|
+
}
|
|
46
|
+
if err = ctx.Scope.Check(ctx.Context(), "files:read", ResourceOwnership{ScopeType: ScopeSpace, WorkspaceID: 2, SpaceID: 8}); err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
intent, err := ctx.Billing.CreatePaymentOrder(ctx.Context(), PaymentOrderRequest{BusinessOrderID: "bo_1", AmountMinor: 123, IdempotencyKey: "pay-1"})
|
|
50
|
+
if err != nil || intent.OrderID != "po_1" || intent.AmountMinor != 123 {
|
|
51
|
+
t.Fatalf("intent = %#v, err = %v", intent, err)
|
|
52
|
+
}
|
|
53
|
+
session, err := ctx.Billing.StartAIBilling(ctx.Context(), AIBillingStartRequest{IdempotencyKey: "ai-1", EstimatedMinor: 10})
|
|
54
|
+
if err != nil || session.ID != "bs_1" || session.ReservedMinor != 10 {
|
|
55
|
+
t.Fatalf("session = %#v, err = %v", session, err)
|
|
56
|
+
}
|
|
57
|
+
if _, err = ctx.Billing.ConsumeEntitlement(ctx.Context(), EntitlementConsumeRequest{Code: "agent.calls", Quantity: 1, IdempotencyKey: "consume-1"}); err != nil {
|
|
58
|
+
t.Fatal(err)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
wantOps := []string{"files.asset.upload", "files.asset.download", "resource.context.current", "scope.permission.check", "billing.payment.create", "billing.ai.session.start", "billing.entitlement.consume"}
|
|
62
|
+
if !reflect.DeepEqual(client.operations, wantOps) {
|
|
63
|
+
t.Fatalf("operations = %#v, want %#v", client.operations, wantOps)
|
|
64
|
+
}
|
|
65
|
+
uploadArgs := client.args[0].(map[string]any)
|
|
66
|
+
if uploadArgs["content_base64"] != "aGVsbG8=" {
|
|
67
|
+
t.Fatalf("file upload is not wire-safe: %#v", uploadArgs)
|
|
68
|
+
}
|
|
69
|
+
args, ok := client.args[4].(PaymentOrderRequest)
|
|
70
|
+
if !ok || args.IdempotencyKey != "pay-1" {
|
|
71
|
+
t.Fatalf("payment args = %#v", client.args[4])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func TestAdminResourceFacadeMarksElevatedCalls(t *testing.T) {
|
|
76
|
+
client := &contractClient{}
|
|
77
|
+
ctx := NewContext(context.Background(), nil, nil, nil, client)
|
|
78
|
+
if _, err := ctx.Admin.Billing.ListPaymentOrders(ctx.Context(), nil); err != nil {
|
|
79
|
+
t.Fatal(err)
|
|
80
|
+
}
|
|
81
|
+
args, ok := client.args[0].(map[string]any)
|
|
82
|
+
if !ok || args["__draftgo_admin"] != true {
|
|
83
|
+
t.Fatalf("admin marker missing from %#v", client.args[0])
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func TestBillingMoneyOperationsKeepMinorUnitsAndIdempotency(t *testing.T) {
|
|
88
|
+
client := &contractClient{}
|
|
89
|
+
ctx := NewContext(context.Background(), nil, nil, nil, client)
|
|
90
|
+
request := MoneyOperationRequest{AmountMinor: 99, IdempotencyKey: "money-1", RequestID: "request-1"}
|
|
91
|
+
if _, err := ctx.Billing.GetAccount(ctx.Context()); err != nil {
|
|
92
|
+
t.Fatal(err)
|
|
93
|
+
}
|
|
94
|
+
if _, err := ctx.Billing.DebitAccount(ctx.Context(), request); err != nil {
|
|
95
|
+
t.Fatal(err)
|
|
96
|
+
}
|
|
97
|
+
if _, err := ctx.Billing.HoldFunds(ctx.Context(), request); err != nil {
|
|
98
|
+
t.Fatal(err)
|
|
99
|
+
}
|
|
100
|
+
if err := ctx.Billing.ReleaseHold(ctx.Context(), HoldOperationRequest{HoldID: "hold-1", IdempotencyKey: "release-1"}); err != nil {
|
|
101
|
+
t.Fatal(err)
|
|
102
|
+
}
|
|
103
|
+
want := []string{"billing.account.get", "billing.account.debit", "billing.account.hold", "billing.account.hold.release"}
|
|
104
|
+
if !reflect.DeepEqual(client.operations, want) {
|
|
105
|
+
t.Fatalf("operations = %#v, want %#v", client.operations, want)
|
|
106
|
+
}
|
|
107
|
+
getArgs := client.args[0].(map[string]any)
|
|
108
|
+
if len(getArgs) != 0 {
|
|
109
|
+
t.Fatalf("account get args = %#v, want no client-selected account", getArgs)
|
|
110
|
+
}
|
|
111
|
+
got := client.args[1].(MoneyOperationRequest)
|
|
112
|
+
if got.AmountMinor != 99 || got.IdempotencyKey != "money-1" || got.RequestID != "request-1" {
|
|
113
|
+
t.Fatalf("money contract = %#v", got)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package sdk
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestFileStoreClientExposesCompleteFolderAndBindingLifecycle(t *testing.T) {
|
|
9
|
+
recorder := &scopeRecordingClient{}
|
|
10
|
+
client := fileStoreClient{platformClient{client: recorder}}
|
|
11
|
+
ctx := context.Background()
|
|
12
|
+
root := int64(0)
|
|
13
|
+
name := "renamed"
|
|
14
|
+
|
|
15
|
+
if _, err := client.UpdateFolder(ctx, 4, FileFolderUpdateInput{Name: &name, ParentID: &root}); err != nil || recorder.operation != "files.folder.update" {
|
|
16
|
+
t.Fatalf("folder update operation=%q args=%#v err=%v", recorder.operation, recorder.args, err)
|
|
17
|
+
}
|
|
18
|
+
data := recorder.args.(map[string]any)["data"].(FileFolderUpdateInput)
|
|
19
|
+
if data.ParentID == nil || *data.ParentID != 0 {
|
|
20
|
+
t.Fatalf("root move was lost: %#v", data)
|
|
21
|
+
}
|
|
22
|
+
if err := client.DeleteFolder(ctx, 4, true); err != nil || recorder.operation != "files.folder.delete" || recorder.args.(map[string]any)["force"] != true {
|
|
23
|
+
t.Fatalf("folder purge operation=%q args=%#v err=%v", recorder.operation, recorder.args, err)
|
|
24
|
+
}
|
|
25
|
+
if _, err := client.RestoreFolder(ctx, 4); err != nil || recorder.operation != "files.folder.restore" {
|
|
26
|
+
t.Fatalf("folder restore operation=%q err=%v", recorder.operation, err)
|
|
27
|
+
}
|
|
28
|
+
if _, err := client.ListBindings(ctx, 8, true); err != nil || recorder.operation != "files.binding.list" || recorder.args.(map[string]any)["include_released"] != true {
|
|
29
|
+
t.Fatalf("binding list operation=%q args=%#v err=%v", recorder.operation, recorder.args, err)
|
|
30
|
+
}
|
|
31
|
+
if err := client.Unbind(ctx, FileBindingRequest{AssetID: 8, BindingType: "knowledge_base", BindingID: 9, Relation: "source"}); err != nil || recorder.operation != "files.binding.release" {
|
|
32
|
+
t.Fatalf("binding release operation=%q args=%#v err=%v", recorder.operation, recorder.args, err)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestFileStoreClientUsesVerifiedHostScope(t *testing.T) {
|
|
37
|
+
recorder := &scopeRecordingClient{}
|
|
38
|
+
client := fileStoreClient{platformClient{client: recorder}}
|
|
39
|
+
ctx := context.Background()
|
|
40
|
+
|
|
41
|
+
folder := FileFolderInput{Name: "Reports"}
|
|
42
|
+
if _, err := client.CreateFolder(ctx, folder); err != nil || recorder.operation != "files.folder.create" {
|
|
43
|
+
t.Fatalf("folder create operation=%q err=%v", recorder.operation, err)
|
|
44
|
+
}
|
|
45
|
+
gotFolder := recorder.args.(FileFolderInput)
|
|
46
|
+
if gotFolder.Name != "Reports" {
|
|
47
|
+
t.Fatalf("folder=%#v", gotFolder)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if _, err := client.Upload(ctx, FileUploadRequest{Filename: "a.txt", Content: []byte("a")}); err != nil {
|
|
51
|
+
t.Fatal(err)
|
|
52
|
+
}
|
|
53
|
+
gotUpload := recorder.args.(map[string]any)
|
|
54
|
+
if _, exists := gotUpload["scope_type"]; exists {
|
|
55
|
+
t.Fatalf("upload accepted client-selected scope=%#v", gotUpload)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package sdk
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
type scopeRecordingClient struct {
|
|
9
|
+
operation string
|
|
10
|
+
args any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func (client *scopeRecordingClient) Call(_ context.Context, operation string, args any) (any, error) {
|
|
14
|
+
client.operation, client.args = operation, args
|
|
15
|
+
switch operation {
|
|
16
|
+
case "resource.context.current":
|
|
17
|
+
return map[string]any{"scope_type": ScopeSpace, "workspace_id": float64(2), "space_id": float64(8)}, nil
|
|
18
|
+
case "scope.principal.current":
|
|
19
|
+
return map[string]any{"subject_type": "service", "subject_id": "service:42", "username": "publisher", "is_admin": true}, nil
|
|
20
|
+
default:
|
|
21
|
+
return map[string]any{"ok": true}, nil
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestScopeClientUsesResourceAndPermissionOperations(t *testing.T) {
|
|
26
|
+
recorder := &scopeRecordingClient{}
|
|
27
|
+
client := scopeClient{platformClient{client: recorder}}
|
|
28
|
+
ctx := context.Background()
|
|
29
|
+
|
|
30
|
+
current, err := client.Current(ctx)
|
|
31
|
+
if err != nil || recorder.operation != "resource.context.current" {
|
|
32
|
+
t.Fatalf("current operation=%q err=%v", recorder.operation, err)
|
|
33
|
+
}
|
|
34
|
+
if current.ScopeType != ScopeSpace || current.WorkspaceID != 2 || current.SpaceID != 8 {
|
|
35
|
+
t.Fatalf("current=%#v", current)
|
|
36
|
+
}
|
|
37
|
+
principal, err := client.CurrentPrincipal(ctx)
|
|
38
|
+
if err != nil || recorder.operation != "scope.principal.current" {
|
|
39
|
+
t.Fatalf("principal operation=%q err=%v", recorder.operation, err)
|
|
40
|
+
}
|
|
41
|
+
if principal.SubjectType != "service" || principal.SubjectID != "service:42" || principal.Username != "publisher" || !principal.IsAdmin {
|
|
42
|
+
t.Fatalf("principal=%#v", principal)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resource := ResourceOwnership{ScopeType: ScopeSpace, WorkspaceID: 2, SpaceID: 8, OwnerUserID: 7}
|
|
46
|
+
if err := client.Check(ctx, "files:read", resource); err != nil || recorder.operation != "scope.permission.check" {
|
|
47
|
+
t.Fatalf("check operation=%q err=%v", recorder.operation, err)
|
|
48
|
+
}
|
|
49
|
+
args := recorder.args.(map[string]any)
|
|
50
|
+
if args["permission"] != "files:read" || args["resource"].(ResourceOwnership) != resource {
|
|
51
|
+
t.Fatalf("check args=%#v", args)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
platform := ResourceOwnership{ScopeType: ScopePlatform}
|
|
55
|
+
if err := client.Check(ctx, "users:read", platform); err != nil {
|
|
56
|
+
t.Fatal(err)
|
|
57
|
+
}
|
|
58
|
+
if args := recorder.args.(map[string]any); args["resource"].(ResourceOwnership) != platform {
|
|
59
|
+
t.Fatalf("platform check args=%#v", args)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestContextExposesResourceContextWithoutLegacyOrganization(t *testing.T) {
|
|
64
|
+
recorder := &scopeRecordingClient{}
|
|
65
|
+
ctx := NewContext(context.Background(), nil, nil, nil, recorder)
|
|
66
|
+
if ctx.Scope == nil || ctx.Admin.Scope == nil {
|
|
67
|
+
t.Fatal("scope SDK facade is unavailable")
|
|
68
|
+
}
|
|
69
|
+
if _, err := ctx.Scope.Current(context.Background()); err != nil || recorder.operation != "resource.context.current" {
|
|
70
|
+
t.Fatalf("scope current operation=%q err=%v", recorder.operation, err)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func TestContextCarriesPersistedResourceAndPrincipal(t *testing.T) {
|
|
75
|
+
recorder := &scopeRecordingClient{}
|
|
76
|
+
ctx := NewContextWithResource(context.Background(), nil, nil, nil, recorder,
|
|
77
|
+
ResourceContext{ScopeType: ScopeSpace, WorkspaceID: 11, SpaceID: 13},
|
|
78
|
+
ScopePrincipal{SubjectType: "service", SubjectID: "custom-service:7", Username: "Service-Script"})
|
|
79
|
+
if ctx.Platform || ctx.WorkspaceID != 11 || ctx.SpaceID != 13 || ctx.Principal.SubjectID != "custom-service:7" {
|
|
80
|
+
t.Fatalf("runtime context = %+v", ctx)
|
|
81
|
+
}
|
|
82
|
+
platform := NewContextWithResource(context.Background(), nil, nil, nil, recorder,
|
|
83
|
+
ResourceContext{ScopeType: ScopePlatform}, ScopePrincipal{SubjectType: "service", SubjectID: "custom-service:8"})
|
|
84
|
+
if !platform.Platform || platform.WorkspaceID != 0 || platform.SpaceID != 0 {
|
|
85
|
+
t.Fatalf("platform runtime context = %+v", platform)
|
|
86
|
+
}
|
|
87
|
+
}
|