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.
Files changed (79) hide show
  1. package/README.md +87 -11
  2. package/package.json +9 -4
  3. package/resources/custom-service-sdk/ai.go +520 -0
  4. package/resources/custom-service-sdk/ai_test.go +156 -0
  5. package/resources/custom-service-sdk/auth_test.go +56 -0
  6. package/resources/custom-service-sdk/billing.go +596 -0
  7. package/resources/custom-service-sdk/billing_test.go +150 -0
  8. package/resources/custom-service-sdk/go.mod +3 -0
  9. package/resources/custom-service-sdk/manifest.json +77 -0
  10. package/resources/custom-service-sdk/platform.go +345 -0
  11. package/resources/custom-service-sdk/platform_logger_test.go +24 -0
  12. package/resources/custom-service-sdk/registration_test.go +39 -0
  13. package/resources/custom-service-sdk/resources.go +246 -0
  14. package/resources/custom-service-sdk/resources_billing_test.go +115 -0
  15. package/resources/custom-service-sdk/resources_files_test.go +57 -0
  16. package/resources/custom-service-sdk/resources_scope_test.go +87 -0
  17. package/resources/custom-service-sdk/sdk.go +208 -0
  18. package/resources/skill/SKILL.md +36 -88
  19. package/resources/skill/init/SKILL.md +4 -4
  20. package/resources/skill/manifest.json +5 -1
  21. package/resources/skill/references/aihub.md +25 -2
  22. package/resources/skill/references/app-api.md +56 -6
  23. package/resources/skill/references/architecture.md +2 -2
  24. package/resources/skill/references/chat-sdk.md +4 -2
  25. package/resources/skill/references/checkout.md +17 -3
  26. package/resources/skill/references/custom-services.md +112 -47
  27. package/resources/skill/references/data.md +19 -4
  28. package/resources/skill/references/delivery.md +33 -0
  29. package/resources/skill/references/diagnostics.md +51 -0
  30. package/resources/skill/references/frontend.md +34 -46
  31. package/resources/skill/references/mcp.md +33 -5
  32. package/resources/skill/references/methods.md +189 -0
  33. package/resources/skill/references/modules.md +37 -9
  34. package/resources/skill/references/runtime.md +23 -1
  35. package/src/cli.js +24 -0
  36. package/src/commandRegistry.js +9 -1
  37. package/src/commands/api.js +21 -10
  38. package/src/commands/apiKey.js +34 -0
  39. package/src/commands/capabilities.js +93 -0
  40. package/src/commands/checkout.js +1 -1
  41. package/src/commands/commit.js +1 -1
  42. package/src/commands/components.js +550 -0
  43. package/src/commands/conflict.js +1 -1
  44. package/src/commands/connect.js +18 -8
  45. package/src/commands/customService.js +20 -4
  46. package/src/commands/dataRange.js +33 -0
  47. package/src/commands/delete.js +12 -1
  48. package/src/commands/diff.js +18 -2
  49. package/src/commands/grant.js +29 -0
  50. package/src/commands/group.js +38 -0
  51. package/src/commands/help.js +64 -20
  52. package/src/commands/init.js +3 -3
  53. package/src/commands/map.js +145 -17
  54. package/src/commands/mcp.js +2 -2
  55. package/src/commands/reconcile.js +1 -1
  56. package/src/commands/role.js +32 -0
  57. package/src/commands/space.js +41 -0
  58. package/src/commands/status.js +110 -7
  59. package/src/commands/update.js +23 -11
  60. package/src/commands/verify.js +75 -0
  61. package/src/commands/worklog.js +6 -2
  62. package/src/consoleEncoding.js +34 -0
  63. package/src/contractCompatibility.js +57 -0
  64. package/src/customServices.js +138 -18
  65. package/src/diffReport.js +106 -0
  66. package/src/index.js +2 -0
  67. package/src/localRuntime/compose.js +14 -17
  68. package/src/localRuntime/index.js +22 -23
  69. package/src/localRuntime/services.js +27 -36
  70. package/src/mcp/client.js +11 -2
  71. package/src/mcp/protocol.js +22 -2
  72. package/src/mcp/tools.js +14 -1
  73. package/src/platforms.js +9 -0
  74. package/src/projectConfig.js +6 -4
  75. package/src/releaseInstall.js +105 -0
  76. package/src/updateCheck.js +48 -28
  77. package/src/worklog.js +2 -1
  78. package/src/worktree/backend.js +1 -1
  79. 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
+ }