@specsage/cli 0.1.0
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/bin/specsage.js +36 -0
- package/lib/browser.js +809 -0
- package/lib/cli.rb +140 -0
- package/lib/dialogs.js +106 -0
- package/lib/results_uploader.rb +133 -0
- package/lib/runner.rb +613 -0
- package/lib/step_client.rb +121 -0
- package/package.json +33 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "base64"
|
|
7
|
+
|
|
8
|
+
class StepClient
|
|
9
|
+
class StepError < StandardError; end
|
|
10
|
+
|
|
11
|
+
attr_reader :server_run_id
|
|
12
|
+
|
|
13
|
+
def initialize(base_url:, server_run_id:, api_key: nil)
|
|
14
|
+
@base_url = base_url
|
|
15
|
+
@server_run_id = server_run_id
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Upload video for a scenario
|
|
20
|
+
# video_data: binary video data
|
|
21
|
+
# scenario_id: the scenario this video belongs to
|
|
22
|
+
def upload_video(scenario_id:, video_data:)
|
|
23
|
+
return unless video_data
|
|
24
|
+
|
|
25
|
+
# Single request: send base64-encoded video, server stores file and creates record
|
|
26
|
+
post("/api/runs/#{@server_run_id}/video", {
|
|
27
|
+
scenario_id: scenario_id,
|
|
28
|
+
video_base64: Base64.strict_encode64(video_data)
|
|
29
|
+
})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set a verdict directly on the server (for client-side errors like step limit exceeded)
|
|
33
|
+
# status: "ERROR", "PASS", or "FAIL"
|
|
34
|
+
# reason: explanation string
|
|
35
|
+
def set_verdict(scenario_id:, status:, reason:)
|
|
36
|
+
post("/api/runs/#{@server_run_id}/step", {
|
|
37
|
+
scenario_id: scenario_id,
|
|
38
|
+
client_verdict: { status: status, reason: reason }
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Submit a step to the server
|
|
43
|
+
# Returns: { action: Hash, step_number: Integer, continue: Boolean }
|
|
44
|
+
def submit_step(scenario_id:, screenshot_base64:, elements:, previous_action: nil, action_result: nil)
|
|
45
|
+
body = {
|
|
46
|
+
scenario_id: scenario_id,
|
|
47
|
+
screenshot_base64: screenshot_base64,
|
|
48
|
+
elements: elements
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
body[:previous_action] = previous_action if previous_action
|
|
52
|
+
body[:action_result] = action_result if action_result
|
|
53
|
+
|
|
54
|
+
response = post("/api/runs/#{@server_run_id}/step", body)
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
action: response["action"],
|
|
58
|
+
step_number: response["step_number"],
|
|
59
|
+
max_steps: response["max_steps"],
|
|
60
|
+
continue: response["continue"],
|
|
61
|
+
credentials: response["credentials"] || {}
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def post(path, body)
|
|
68
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
69
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
70
|
+
http.use_ssl = uri.scheme == "https"
|
|
71
|
+
http.open_timeout = 10
|
|
72
|
+
http.read_timeout = 120 # LLM calls can take a while
|
|
73
|
+
|
|
74
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
75
|
+
request["Content-Type"] = "application/json"
|
|
76
|
+
request["Accept"] = "application/json"
|
|
77
|
+
request["Authorization"] = "Bearer #{@api_key}" if @api_key
|
|
78
|
+
request.body = body.to_json
|
|
79
|
+
|
|
80
|
+
response = http.request(request)
|
|
81
|
+
handle_response(response, path)
|
|
82
|
+
rescue Errno::ECONNREFUSED
|
|
83
|
+
raise StepError, "Could not connect to server at #{@base_url}"
|
|
84
|
+
rescue Net::ReadTimeout
|
|
85
|
+
raise StepError, "Server timeout - LLM may be taking too long"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def handle_response(response, path)
|
|
89
|
+
parsed = parse_response_body(response)
|
|
90
|
+
|
|
91
|
+
case response
|
|
92
|
+
when Net::HTTPSuccess
|
|
93
|
+
parsed
|
|
94
|
+
when Net::HTTPNotFound
|
|
95
|
+
raise StepError, "Not found: #{parsed['error'] || path}"
|
|
96
|
+
when Net::HTTPServiceUnavailable
|
|
97
|
+
raise StepError, "Service unavailable: #{parsed['error']}"
|
|
98
|
+
when Net::HTTPUnprocessableEntity
|
|
99
|
+
raise StepError, "Validation error: #{parsed['error']}"
|
|
100
|
+
else
|
|
101
|
+
raise StepError, "Server error #{response.code}: #{parsed['error'] || response.message}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_response_body(response)
|
|
106
|
+
body = response.body.to_s
|
|
107
|
+
content_type = response["Content-Type"].to_s
|
|
108
|
+
|
|
109
|
+
return {} if body.empty?
|
|
110
|
+
|
|
111
|
+
unless content_type.include?("application/json")
|
|
112
|
+
snippet = body.length > 200 ? "#{body[0, 200]}..." : body
|
|
113
|
+
raise StepError, "Expected JSON response but got #{content_type.inspect}: #{snippet}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
JSON.parse(body)
|
|
117
|
+
rescue JSON::ParserError => e
|
|
118
|
+
snippet = body.length > 200 ? "#{body[0, 200]}..." : body
|
|
119
|
+
raise StepError, "Invalid JSON response from server: #{e.message}. Body: #{snippet}"
|
|
120
|
+
end
|
|
121
|
+
end
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@specsage/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SpecSage CLI - AI-powered end-to-end testing automation (Node wrapper for Ruby CLI)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"specsage": "./bin/specsage.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"testing",
|
|
18
|
+
"e2e",
|
|
19
|
+
"automation",
|
|
20
|
+
"playwright",
|
|
21
|
+
"ai"
|
|
22
|
+
],
|
|
23
|
+
"author": "SpecSage",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/specsage/specsage.git",
|
|
28
|
+
"directory": "packages/cli"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|