Skip to content

Commit 19e76b2

Browse files
committed
fix conflicts
2 parents e690113 + cdd2abb commit 19e76b2

File tree

12 files changed

+3054
-3199
lines changed

12 files changed

+3054
-3199
lines changed

cmd/run.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,8 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter
402402
default:
403403
o.limiter.Accept()
404404

405-
ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout)
405+
ctxWithTimeout, cancel := context.WithTimeout(ctx, o.requestTimeout)
406+
defer cancel() // Ensure context is always cancelled when leaving this scope
406407
ctxWithTimeout = context.WithValue(ctxWithTimeout, runner.ContextKey("").ParentDir(), loader.GetContext())
407408

408409
output, err = suiteRunner.RunTestCase(&testCase, dataContext, ctxWithTimeout)

console/atest-desktop/forge.config.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ module.exports = {
5151
ui: {
5252
"enabled": true,
5353
"chooseDirectory": true
54+
},
55+
beforeCreate: (msiCreator) => {
56+
// Add installation directory to system PATH
57+
msiCreator.wixTemplate = msiCreator.wixTemplate.replace(
58+
'</Product>',
59+
` <Property Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
60+
<CustomAction Id="AddToPath" Property="PATH" Value="[INSTALLDIR]" Execute="immediate" />
61+
<CustomAction Id="RemoveFromPath" Property="PATH" Value="[INSTALLDIR]" Execute="immediate" />
62+
63+
<InstallExecuteSequence>
64+
<Custom Action="AddToPath" After="InstallFiles">NOT Installed</Custom>
65+
<Custom Action="RemoveFromPath" Before="RemoveFiles">REMOVE="ALL"</Custom>
66+
</InstallExecuteSequence>
67+
68+
<Component Id="PathComponent" Guid="*" Directory="INSTALLDIR">
69+
<Environment Id="PATH" Name="PATH" Value="[INSTALLDIR]" Permanent="no" Part="last" Action="set" System="yes" />
70+
</Component>
71+
72+
</Product>`
73+
);
5474
}
5575
}
5676
}

pkg/mock/in_memory.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,15 @@ func runWebhook(ctx context.Context, objCtx interface{}, wh *Webhook) (err error
596596
}
597597
}
598598

599+
if wh.Request.BodyFromFile != "" {
600+
if data, readErr := os.ReadFile(wh.Request.BodyFromFile); readErr != nil {
601+
memLogger.Error(readErr, "failed to read file", "file", wh.Request.BodyFromFile)
602+
return
603+
} else {
604+
wh.Request.Body = string(data)
605+
}
606+
}
607+
599608
var payload io.Reader
600609
payload, err = render.RenderAsReader("mock webhook server payload", wh.Request.Body, wh)
601610
if err != nil {

pkg/mock/types.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ type Item struct {
2929
}
3030

3131
type Request struct {
32-
Protocol string `yaml:"protocol" json:"protocol"`
33-
Path string `yaml:"path" json:"path"`
34-
Method string `yaml:"method" json:"method"`
35-
Header map[string]string `yaml:"header" json:"header"`
36-
Body string `yaml:"body" json:"body"`
32+
Protocol string `yaml:"protocol" json:"protocol"`
33+
Path string `yaml:"path" json:"path"`
34+
Method string `yaml:"method" json:"method"`
35+
Header map[string]string `yaml:"header" json:"header"`
36+
Body string `yaml:"body" json:"body"`
37+
BodyFromFile string `yaml:"bodyFromFile" json:"bodyFromFile"`
3738
}
3839

3940
type RequestWithAuth struct {

pkg/server/ai_interface.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2025 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package server
18+
19+
// AI Plugin Communication Interface Standards
20+
// AI plugins use the existing testing.Loader.Query(map[string]string) interface
21+
22+
// Standard AI plugin communication methods
23+
const (
24+
AIMethodGenerate = "ai.generate" // Generate content from prompt
25+
AIMethodCapabilities = "ai.capabilities" // Get plugin capabilities
26+
)
27+
28+
// AI Plugin Query Parameter Standards
29+
// AI plugins are called using loader.Query(query map[string]string) with these parameters:
30+
31+
// For ai.generate:
32+
// - "method": "ai.generate"
33+
// - "model": model identifier (e.g., "gpt-4", "claude")
34+
// - "prompt": the prompt or instruction
35+
// - "config": optional JSON configuration string (e.g., `{"temperature": 0.7, "max_tokens": 1000}`)
36+
37+
// For ai.capabilities:
38+
// - "method": "ai.capabilities"
39+
40+
// AI Plugin Response Standards
41+
// AI plugins return response through testing.DataResult.Pairs with these keys:
42+
43+
// For successful ai.generate:
44+
// - "content": the generated content
45+
// - "meta": optional JSON metadata string (model info, timing, etc.)
46+
// - "success": "true"
47+
48+
// For successful ai.capabilities:
49+
// - "capabilities": JSON string containing plugin capabilities
50+
// - "models": JSON array of supported models (fallback if capabilities not available)
51+
// - "features": JSON array of supported features (fallback)
52+
// - "description": plugin description (fallback)
53+
// - "version": plugin version (fallback)
54+
// - "success": "true"
55+
56+
// For errors:
57+
// - "error": error message
58+
// - "success": "false"
59+
60+
// Plugin Discovery
61+
// AI plugins are identified by having "ai" in their categories field:
62+
// categories: ["ai"]
63+
64+
// Usage Examples:
65+
//
66+
// Get AI plugins:
67+
// stores, err := server.GetStores(ctx, &SimpleQuery{Kind: "ai"})
68+
//
69+
// Call AI plugin:
70+
// loader, err := server.getLoaderByStoreName("my-ai-plugin")
71+
// result, err := loader.Query(map[string]string{
72+
// "method": "ai.generate",
73+
// "model": "gpt-4",
74+
// "prompt": "Hello world",
75+
// "config": `{"temperature": 0.7}`,
76+
// })
77+
// content := result.Pairs["content"]
78+
79+
// Documentation structures (for reference only, actual types are generated from proto)
80+
// See server.proto for the actual message definitions:
81+
//
82+
// AIRequest fields:
83+
// - plugin_name: AI plugin name
84+
// - model: Model identifier (e.g., "gpt-4", "claude")
85+
// - prompt: The prompt or instruction
86+
// - config: JSON configuration string (optional)
87+
//
88+
// AIResponse fields:
89+
// - content: Generated content
90+
// - meta: JSON metadata string (optional)
91+
// - success: Whether the call succeeded
92+
// - error: Error message if failed
93+
//
94+
// AICapabilitiesResponse fields:
95+
// - models: Supported models
96+
// - features: Supported features
97+
// - description: Plugin description
98+
// - version: Plugin version
99+
// - success: Whether the call succeeded
100+
// - error: Error message if failed

pkg/server/remote_server.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,3 +1765,147 @@ func (s *UniqueSlice[T]) GetAll() []T {
17651765
}
17661766

17671767
var errNoTestSuiteFound = errors.New("no test suite found")
1768+
1769+
// CallAI calls an AI plugin to generate content
1770+
func (s *server) CallAI(ctx context.Context, req *AIRequest) (*AIResponse, error) {
1771+
if req.GetPluginName() == "" {
1772+
return &AIResponse{
1773+
Success: false,
1774+
Error: "plugin_name is required",
1775+
}, nil
1776+
}
1777+
1778+
// Get the loader for the AI plugin
1779+
loader, err := s.getLoaderByStoreName(req.GetPluginName())
1780+
if err != nil {
1781+
return &AIResponse{
1782+
Success: false,
1783+
Error: fmt.Sprintf("failed to get AI plugin '%s': %v", req.GetPluginName(), err),
1784+
}, nil
1785+
}
1786+
defer loader.Close()
1787+
1788+
// Prepare query parameters
1789+
query := map[string]string{
1790+
"method": AIMethodGenerate,
1791+
"model": req.GetModel(),
1792+
"prompt": req.GetPrompt(),
1793+
}
1794+
1795+
// Add config (always include, even if empty)
1796+
query["config"] = req.GetConfig()
1797+
1798+
// Call the plugin using the Query interface
1799+
result, err := loader.Query(query)
1800+
if err != nil {
1801+
return &AIResponse{
1802+
Success: false,
1803+
Error: fmt.Sprintf("AI plugin call failed: %v", err),
1804+
}, nil
1805+
}
1806+
1807+
// Extract response from result
1808+
response := &AIResponse{
1809+
Success: true,
1810+
}
1811+
1812+
// Get content from result
1813+
if content, ok := result.Pairs["content"]; ok {
1814+
response.Content = content
1815+
}
1816+
1817+
// Get metadata if available
1818+
if meta, ok := result.Pairs["meta"]; ok {
1819+
response.Meta = meta
1820+
}
1821+
1822+
// Check for errors from plugin
1823+
if errorMsg, ok := result.Pairs["error"]; ok && errorMsg != "" {
1824+
response.Success = false
1825+
response.Error = errorMsg
1826+
}
1827+
1828+
// Check success flag from plugin
1829+
if success, ok := result.Pairs["success"]; ok && success == "false" {
1830+
response.Success = false
1831+
if response.Error == "" {
1832+
response.Error = "AI plugin returned failure status"
1833+
}
1834+
}
1835+
1836+
remoteServerLogger.Info("AI plugin called",
1837+
"plugin", req.GetPluginName(),
1838+
"model", req.GetModel(),
1839+
"success", response.GetSuccess())
1840+
1841+
return response, nil
1842+
}
1843+
1844+
// GetAICapabilities gets the capabilities of an AI plugin
1845+
func (s *server) GetAICapabilities(ctx context.Context, req *AICapabilitiesRequest) (*AICapabilitiesResponse, error) {
1846+
if req.GetPluginName() == "" {
1847+
return &AICapabilitiesResponse{
1848+
Success: false,
1849+
Error: "plugin_name is required",
1850+
}, nil
1851+
}
1852+
1853+
// Get the loader for the AI plugin
1854+
loader, err := s.getLoaderByStoreName(req.GetPluginName())
1855+
if err != nil {
1856+
return &AICapabilitiesResponse{
1857+
Success: false,
1858+
Error: fmt.Sprintf("failed to get AI plugin '%s': %v", req.GetPluginName(), err),
1859+
}, nil
1860+
}
1861+
defer loader.Close()
1862+
1863+
// Query for capabilities
1864+
query := map[string]string{
1865+
"method": AIMethodCapabilities,
1866+
}
1867+
1868+
result, err := loader.Query(query)
1869+
if err != nil {
1870+
return &AICapabilitiesResponse{
1871+
Success: false,
1872+
Error: fmt.Sprintf("failed to get capabilities: %v", err),
1873+
}, nil
1874+
}
1875+
1876+
// Build response from result
1877+
response := &AICapabilitiesResponse{
1878+
Success: true,
1879+
}
1880+
1881+
// Parse capabilities from result
1882+
if models, ok := result.Pairs["models"]; ok {
1883+
// Try to parse as JSON array first
1884+
response.Models = strings.Split(models, ",")
1885+
}
1886+
1887+
if features, ok := result.Pairs["features"]; ok {
1888+
response.Features = strings.Split(features, ",")
1889+
}
1890+
1891+
if description, ok := result.Pairs["description"]; ok {
1892+
response.Description = description
1893+
}
1894+
1895+
if version, ok := result.Pairs["version"]; ok {
1896+
response.Version = version
1897+
}
1898+
1899+
// Check for errors
1900+
if errorMsg, ok := result.Pairs["error"]; ok && errorMsg != "" {
1901+
response.Success = false
1902+
response.Error = errorMsg
1903+
}
1904+
1905+
remoteServerLogger.Info("AI plugin capabilities retrieved",
1906+
"plugin", req.GetPluginName(),
1907+
"models", len(response.GetModels()),
1908+
"features", len(response.GetFeatures()))
1909+
1910+
return response, nil
1911+
}

0 commit comments

Comments
 (0)