@specsage/cli 0.1.11 → 0.1.13

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/lib/browser.js CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // DO NOT EDIT packages/cli/lib/browser.js - it is copied from this file during npm publish.
4
+ // See bin/publish_npm_package for details.
5
+
3
6
  import { chromium } from "playwright";
4
7
  import fs from "fs";
5
8
  import path from "path";
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared base error type for step clients (StepClient, DirectStepClient).
4
+ # Runner can rescue this base type to handle errors from either client.
5
+ # Each client defines its own StepError subclass for API stability.
6
+ class StepClientError < StandardError; end
package/lib/dialogs.js CHANGED
@@ -2,6 +2,9 @@
2
2
  * Dialog handling module for SpecSage browser automation.
3
3
  * Captures JavaScript dialogs (alert, confirm, prompt) and exposes them
4
4
  * to the AI agent via visual overlays and pseudo-elements.
5
+ *
6
+ * DO NOT EDIT packages/cli/lib/dialogs.js - it is copied from this file during npm publish.
7
+ * See bin/publish_npm_package for details.
5
8
  */
6
9
 
7
10
  // Pending dialog state
package/lib/runner.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # DO NOT EDIT packages/cli/lib/runner.rb - it is copied from this file during npm publish.
5
+ # See bin/publish_npm_package for details.
6
+
4
7
  require 'json'
5
8
  require 'timeout'
6
9
  require 'fileutils'
@@ -8,6 +11,7 @@ require 'fileutils'
8
11
  # Determine SpecSage home directory for locating resources
9
12
  SPECSAGE_HOME ||= File.expand_path('..', __dir__)
10
13
 
14
+ require_relative 'client_errors'
11
15
  require_relative 'step_client'
12
16
 
13
17
  class Runner
@@ -22,7 +26,8 @@ class Runner
22
26
 
23
27
  # Initialize runner with scenario data from server
24
28
  # @param all_scenarios [Hash] optional map of scenario_id => scenario_data for pre-scenario lookup
25
- def initialize(scenario_data, visible: false, record: false, publisher: nil, server_run_id: nil, all_scenarios: nil)
29
+ # @param step_client [StepClient, DirectStepClient] optional pre-configured client for step processing
30
+ def initialize(scenario_data, visible: false, record: false, publisher: nil, server_run_id: nil, all_scenarios: nil, step_client: nil)
26
31
  @scenario = normalize_scenario_data(scenario_data)
27
32
  @scenario_id = @scenario['id']
28
33
  @scenario_name = @scenario['name'] || @scenario['id'] || 'unnamed'
@@ -35,7 +40,7 @@ class Runner
35
40
  @next_request_id = 1
36
41
  @node_channel_poisoned = false
37
42
  @publisher = publisher
38
- @step_client = nil
43
+ @step_client = step_client # Accept pre-configured client (nil = create StepClient in run())
39
44
  @server_run_id = server_run_id
40
45
  @credentials = {} # Credentials received from server { "NAME" => "value" }
41
46
  @max_steps = nil # Max browser actions allowed, received from server on first step
@@ -48,7 +53,10 @@ class Runner
48
53
 
49
54
  raise ArgumentError, 'server_run_id is required' unless @server_run_id
50
55
 
51
- @step_client = StepClient.new(
56
+ # Only create StepClient if not injected (CLI path uses HTTP, Sidekiq injects DirectStepClient)
57
+ # TODO: Validate publisher presence here when step_client is nil. Currently crashes with
58
+ # NoMethodError on @publisher.base_url if caller omits both step_client and publisher.
59
+ @step_client ||= StepClient.new(
52
60
  base_url: @publisher.base_url,
53
61
  server_run_id: @server_run_id,
54
62
  api_key: @publisher.api_key
@@ -69,7 +77,7 @@ class Runner
69
77
  stop_node_process
70
78
  upload_video
71
79
  cleanup_temp_dir
72
- return
80
+ return 'ERROR'
73
81
  end
74
82
 
75
83
  pre_scenario_name = pre_scenario_data['name'] || pre_scenario_id
@@ -92,14 +100,14 @@ class Runner
92
100
  stop_node_process
93
101
  upload_video
94
102
  cleanup_temp_dir
95
- return
103
+ return 'FAIL'
96
104
  elsif result[:verdict] == 'ERROR'
97
105
  log "Pre-scenario '#{pre_scenario_name}' ERROR - skipping main scenario"
98
106
  send_main_scenario_verdict('PRE_SCENARIO_ERROR', "Pre-scenario '#{pre_scenario_name}' errored: #{result[:reason]}")
99
107
  stop_node_process
100
108
  upload_video
101
109
  cleanup_temp_dir
102
- return
110
+ return 'ERROR'
103
111
  end
104
112
 
105
113
  log "Pre-scenario '#{pre_scenario_name}' PASSED"
@@ -109,7 +117,7 @@ class Runner
109
117
  end
110
118
 
111
119
  # Run the main scenario
112
- run_single_scenario(
120
+ result = run_single_scenario(
113
121
  scenario_id: @scenario_id,
114
122
  scenario_data: @scenario,
115
123
  pre_scenario_for_id: nil,
@@ -119,16 +127,20 @@ class Runner
119
127
  stop_node_process
120
128
  upload_video
121
129
  cleanup_temp_dir
122
- rescue StepClient::StepError => e
130
+
131
+ result[:verdict]
132
+ rescue StepClientError => e
123
133
  send_client_verdict_if_needed('ERROR', "Server error: #{e.message}")
124
134
  stop_node_process
125
135
  upload_video
126
136
  cleanup_temp_dir
137
+ 'ERROR'
127
138
  rescue StandardError => e
128
139
  send_client_verdict_if_needed('ERROR', e.message)
129
140
  stop_node_process
130
141
  upload_video
131
142
  cleanup_temp_dir
143
+ 'ERROR'
132
144
  end
133
145
 
134
146
  # Run a single scenario (either pre-scenario or main scenario)
@@ -238,7 +250,7 @@ class Runner
238
250
  status: status,
239
251
  reason: reason
240
252
  )
241
- rescue StepClient::StepError => e
253
+ rescue StepClientError => e
242
254
  log "Warning: Failed to send main scenario verdict: #{e.message}"
243
255
  end
244
256
 
@@ -580,7 +592,7 @@ class Runner
580
592
  log "Uploading video (#{@video_data.bytesize} bytes)..."
581
593
  @step_client.upload_video(scenario_id: @scenario_id, video_data: @video_data)
582
594
  log "Video uploaded successfully."
583
- rescue StepClient::StepError => e
595
+ rescue StepClientError => e
584
596
  log "Warning: Failed to upload video: #{e.message}"
585
597
  end
586
598
 
@@ -634,7 +646,7 @@ class Runner
634
646
  status: status,
635
647
  reason: reason
636
648
  )
637
- rescue StepClient::StepError => e
649
+ rescue StepClientError => e
638
650
  log "Warning: Failed to send verdict to server: #{e.message}"
639
651
  end
640
652
 
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # DO NOT EDIT packages/cli/lib/step_client.rb - it is copied from this file during npm publish.
4
+ # See bin/publish_npm_package for details.
5
+
3
6
  require "net/http"
4
7
  require "uri"
5
8
  require "json"
6
9
  require "base64"
10
+ require_relative "client_errors"
7
11
 
8
12
  class StepClient
9
- class StepError < StandardError; end
13
+ # Subclass shared error for API stability (supports subclassing, .class checks, .name)
14
+ # Runner can rescue either StepClient::StepError or the base StepClientError
15
+ class StepError < StepClientError; end
10
16
 
11
17
  attr_reader :server_run_id
12
18
 
@@ -56,6 +62,10 @@ class StepClient
56
62
 
57
63
  response = post("/api/runs/#{@server_run_id}/step", body)
58
64
 
65
+ # TODO: Credentials defaulted to {} may change semantics. Server returns nil when no
66
+ # credentials are present (after first step). Returning {} instead of nil may cause
67
+ # downstream code to branch incorrectly (`if credentials` is always truthy). Consider
68
+ # returning nil when server returns nil, or document this as intentional contract.
59
69
  {
60
70
  action: response["action"],
61
71
  step_number: response["step_number"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specsage/cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "SpecSage CLI - AI-powered end-to-end testing automation (Node wrapper for Ruby CLI)",
5
5
  "type": "module",
6
6
  "bin": {