rails_proof 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +54 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1210 -0
- data/Rakefile +3 -0
- data/lib/generators/rails_proof/test/USAGE +8 -0
- data/lib/generators/rails_proof/test/test_generator.rb +542 -0
- data/lib/rails_proof/ai_concern_filter.rb +142 -0
- data/lib/rails_proof/ai_test_executor.rb +263 -0
- data/lib/rails_proof/ai_test_identity.rb +261 -0
- data/lib/rails_proof/ai_test_planner.rb +118 -0
- data/lib/rails_proof/ai_test_validator.rb +65 -0
- data/lib/rails_proof/ai_test_writer.rb +51 -0
- data/lib/rails_proof/controller_inspector.rb +44 -0
- data/lib/rails_proof/controller_test_coverage_plan.rb +79 -0
- data/lib/rails_proof/controller_test_plan.rb +28 -0
- data/lib/rails_proof/controller_test_writer.rb +66 -0
- data/lib/rails_proof/model_inspector.rb +50 -0
- data/lib/rails_proof/model_test_plan.rb +61 -0
- data/lib/rails_proof/open_ai_client.rb +189 -0
- data/lib/rails_proof/railtie.rb +4 -0
- data/lib/rails_proof/review_store.rb +241 -0
- data/lib/rails_proof/target_discovery.rb +145 -0
- data/lib/rails_proof/test_coverage_plan.rb +84 -0
- data/lib/rails_proof/test_inspector.rb +54 -0
- data/lib/rails_proof/test_runner.rb +136 -0
- data/lib/rails_proof/test_writer.rb +87 -0
- data/lib/rails_proof/version.rb +3 -0
- data/lib/rails_proof.rb +13 -0
- data/lib/tasks/rails_proof_tasks.rake +4 -0
- metadata +96 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class AiTestWriter
|
|
3
|
+
class InvalidConcern < StandardError; end
|
|
4
|
+
|
|
5
|
+
attr_reader :test_class_name, :superclass, :concerns
|
|
6
|
+
|
|
7
|
+
def initialize(test_class_name:, superclass:, concerns:)
|
|
8
|
+
@test_class_name = test_class_name
|
|
9
|
+
@superclass = superclass
|
|
10
|
+
@concerns = concerns
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render
|
|
14
|
+
concerns.map do |concern|
|
|
15
|
+
indent(test_code_for(concern), 2)
|
|
16
|
+
end.join("\n\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render_test_file
|
|
20
|
+
[
|
|
21
|
+
'require "test_helper"',
|
|
22
|
+
"",
|
|
23
|
+
"class #{test_class_name} < #{superclass}",
|
|
24
|
+
render,
|
|
25
|
+
"end",
|
|
26
|
+
""
|
|
27
|
+
].join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def test_code_for(concern)
|
|
33
|
+
code = concern[:test_code] || concern["test_code"]
|
|
34
|
+
|
|
35
|
+
unless code.is_a?(String) && code.strip.present?
|
|
36
|
+
raise InvalidConcern,
|
|
37
|
+
"AI test concern must include test_code"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
code.strip
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def indent(source, spaces)
|
|
44
|
+
prefix = " " * spaces
|
|
45
|
+
|
|
46
|
+
source.lines.map do |line|
|
|
47
|
+
line.strip.empty? ? line : "#{prefix}#{line}"
|
|
48
|
+
end.join
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class ControllerInspector
|
|
3
|
+
attr_reader :controller_class, :route_set
|
|
4
|
+
|
|
5
|
+
def initialize(controller_class, route_set: Rails.application.routes)
|
|
6
|
+
@controller_class = controller_class
|
|
7
|
+
@route_set = route_set
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def controller_name
|
|
11
|
+
controller_class.name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def controller_path
|
|
15
|
+
controller_class.controller_path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def actions
|
|
19
|
+
@actions ||= controller_class.action_methods.sort
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def routes
|
|
23
|
+
@routes ||= route_set.routes.filter_map do |route|
|
|
24
|
+
defaults = route.defaults
|
|
25
|
+
|
|
26
|
+
next unless defaults[:controller] == controller_path
|
|
27
|
+
next unless actions.include?(defaults[:action])
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
name: route.name,
|
|
31
|
+
verb: route.verb.to_s,
|
|
32
|
+
path: normalize_path(route.path.spec.to_s),
|
|
33
|
+
action: defaults[:action]
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def normalize_path(path)
|
|
41
|
+
path.delete_suffix("(.:format)")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class ControllerTestCoveragePlan
|
|
3
|
+
attr_reader :controller_test_plan, :test_inspector
|
|
4
|
+
|
|
5
|
+
def initialize(controller_test_plan, test_inspector)
|
|
6
|
+
@controller_test_plan = controller_test_plan
|
|
7
|
+
@test_inspector = test_inspector
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def covered_concerns
|
|
11
|
+
@covered_concerns ||= controller_test_plan.concerns.select do |concern|
|
|
12
|
+
covered?(concern)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def missing_concerns
|
|
17
|
+
@missing_concerns ||= controller_test_plan.concerns.reject do |concern|
|
|
18
|
+
covered?(concern)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def covered_count
|
|
23
|
+
covered_concerns.count
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def missing_count
|
|
27
|
+
missing_concerns.count
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def covered?(concern)
|
|
33
|
+
test_names.any? do |test_name|
|
|
34
|
+
matches_concern?(test_name, concern)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_names
|
|
39
|
+
@test_names ||= test_inspector.test_cases.map do |test_case|
|
|
40
|
+
normalize(test_case[:name])
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def matches_concern?(test_name, concern)
|
|
45
|
+
return false unless concern[:type] == :controller_response
|
|
46
|
+
|
|
47
|
+
action = normalize(concern[:action])
|
|
48
|
+
|
|
49
|
+
test_name.include?(action) &&
|
|
50
|
+
response_language?(test_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def response_language?(test_name)
|
|
54
|
+
[
|
|
55
|
+
"get",
|
|
56
|
+
"post",
|
|
57
|
+
"patch",
|
|
58
|
+
"put",
|
|
59
|
+
"delete",
|
|
60
|
+
"response",
|
|
61
|
+
"respond",
|
|
62
|
+
"success",
|
|
63
|
+
"successful"
|
|
64
|
+
].any? do |word|
|
|
65
|
+
test_name.split.include?(word)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def normalize(value)
|
|
70
|
+
value
|
|
71
|
+
.to_s
|
|
72
|
+
.downcase
|
|
73
|
+
.tr("_", " ")
|
|
74
|
+
.gsub(/[^a-z0-9\s]/, " ")
|
|
75
|
+
.split
|
|
76
|
+
.join(" ")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class ControllerTestPlan
|
|
3
|
+
attr_reader :inspector
|
|
4
|
+
|
|
5
|
+
def initialize(inspector)
|
|
6
|
+
@inspector = inspector
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def concerns
|
|
10
|
+
@concerns ||= inspector.routes.filter_map do |route|
|
|
11
|
+
next if route[:name].to_s.empty?
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
type: :controller_response,
|
|
15
|
+
verb: route[:verb],
|
|
16
|
+
path: route[:path],
|
|
17
|
+
route_name: route[:name],
|
|
18
|
+
action: route[:action],
|
|
19
|
+
description: "#{route[:verb]} #{route[:path]} responds successfully"
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def count
|
|
25
|
+
concerns.count
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class ControllerTestWriter
|
|
3
|
+
attr_reader :controller_class_name, :concerns
|
|
4
|
+
|
|
5
|
+
def initialize(controller_class_name:, concerns:)
|
|
6
|
+
@controller_class_name = controller_class_name
|
|
7
|
+
@concerns = concerns
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render
|
|
11
|
+
concerns.map do |concern|
|
|
12
|
+
indent(render_concern(concern), 2)
|
|
13
|
+
end.join("\n\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_test_file
|
|
17
|
+
[
|
|
18
|
+
'require "test_helper"',
|
|
19
|
+
"",
|
|
20
|
+
"class #{controller_class_name}Test < ActionDispatch::IntegrationTest",
|
|
21
|
+
render,
|
|
22
|
+
"end",
|
|
23
|
+
""
|
|
24
|
+
].join("\n")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def render_concern(concern)
|
|
30
|
+
case concern[:type]
|
|
31
|
+
when :controller_response
|
|
32
|
+
render_response_test(concern)
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"Unsupported controller test concern: #{concern.inspect}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_response_test(concern)
|
|
40
|
+
verb = concern.fetch(:verb).downcase
|
|
41
|
+
route_name = concern.fetch(:route_name)
|
|
42
|
+
action = concern.fetch(:action)
|
|
43
|
+
|
|
44
|
+
unless route_name
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
"Cannot generate controller test without a named route: #{concern.inspect}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
<<~RUBY.chomp
|
|
50
|
+
test "should #{verb} #{action}" do
|
|
51
|
+
#{verb} #{route_name}_url
|
|
52
|
+
|
|
53
|
+
assert_response :success
|
|
54
|
+
end
|
|
55
|
+
RUBY
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def indent(source, spaces)
|
|
59
|
+
prefix = " " * spaces
|
|
60
|
+
|
|
61
|
+
source.lines.map do |line|
|
|
62
|
+
line.strip.empty? ? line : "#{prefix}#{line}"
|
|
63
|
+
end.join
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class ModelInspector
|
|
3
|
+
attr_reader :model_class
|
|
4
|
+
|
|
5
|
+
def initialize(model_class)
|
|
6
|
+
@model_class = model_class
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def model_name
|
|
10
|
+
model_class.name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def table_name
|
|
14
|
+
model_class.table_name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def columns
|
|
18
|
+
@columns ||= model_class.columns.map do |column|
|
|
19
|
+
{
|
|
20
|
+
name: column.name,
|
|
21
|
+
type: column.type,
|
|
22
|
+
null: column.null,
|
|
23
|
+
default: column.default
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def associations
|
|
29
|
+
@associations ||= model_class.reflect_on_all_associations.map do |association|
|
|
30
|
+
{
|
|
31
|
+
macro: association.macro,
|
|
32
|
+
name: association.name,
|
|
33
|
+
class_name: association.class_name,
|
|
34
|
+
foreign_key: association.foreign_key,
|
|
35
|
+
options: association.options
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validators
|
|
41
|
+
@validators ||= model_class.validators.map do |validator|
|
|
42
|
+
{
|
|
43
|
+
class_name: validator.class.name,
|
|
44
|
+
attributes: validator.attributes,
|
|
45
|
+
options: validator.options
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module RailsProof
|
|
2
|
+
class ModelTestPlan
|
|
3
|
+
attr_reader :inspector
|
|
4
|
+
|
|
5
|
+
def initialize(inspector)
|
|
6
|
+
@inspector = inspector
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def concerns
|
|
10
|
+
@concerns ||= association_concerns + validation_concerns
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def count
|
|
14
|
+
concerns.count
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def association_concerns
|
|
20
|
+
inspector.associations.map do |association|
|
|
21
|
+
{
|
|
22
|
+
type: :association,
|
|
23
|
+
macro: association[:macro],
|
|
24
|
+
name: association[:name],
|
|
25
|
+
description: "#{association[:macro]} :#{association[:name]}"
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validation_concerns
|
|
31
|
+
inspector.validators.flat_map do |validator|
|
|
32
|
+
next [] unless presence_validator?(validator)
|
|
33
|
+
|
|
34
|
+
validator[:attributes].filter_map do |attribute|
|
|
35
|
+
next if implicit_belongs_to_presence?(attribute)
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
type: :validation,
|
|
39
|
+
validation: :presence,
|
|
40
|
+
attribute: attribute,
|
|
41
|
+
description: "validates presence of #{attribute}"
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def presence_validator?(validator)
|
|
48
|
+
validator[:class_name].end_with?("PresenceValidator")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def implicit_belongs_to_presence?(attribute)
|
|
52
|
+
belongs_to_association_names.include?(attribute)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def belongs_to_association_names
|
|
56
|
+
@belongs_to_association_names ||= inspector.associations.filter_map do |association|
|
|
57
|
+
association[:name] if association[:macro] == :belongs_to
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module RailsProof
|
|
4
|
+
class OpenAiClient
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
DEFAULT_MODEL = "gpt-5.6"
|
|
8
|
+
|
|
9
|
+
RESPONSE_SCHEMA = {
|
|
10
|
+
type: :object,
|
|
11
|
+
properties: {
|
|
12
|
+
suggestions: {
|
|
13
|
+
type: :array,
|
|
14
|
+
items: {
|
|
15
|
+
type: :object,
|
|
16
|
+
properties: {
|
|
17
|
+
kind: {
|
|
18
|
+
type: :string,
|
|
19
|
+
enum: %w[coverage contract_check]
|
|
20
|
+
},
|
|
21
|
+
name: { type: :string },
|
|
22
|
+
reason: { type: :string },
|
|
23
|
+
test_code: { type: :string }
|
|
24
|
+
},
|
|
25
|
+
required: %w[kind name reason test_code],
|
|
26
|
+
additionalProperties: false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: %w[suggestions],
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
attr_reader :model
|
|
35
|
+
|
|
36
|
+
def initialize(
|
|
37
|
+
model: ENV.fetch("RAILSPROOF_OPENAI_MODEL", DEFAULT_MODEL),
|
|
38
|
+
sdk_client: nil
|
|
39
|
+
)
|
|
40
|
+
@model = model
|
|
41
|
+
@sdk_client = sdk_client
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def suggest_tests(context:)
|
|
45
|
+
response = client.responses.create(
|
|
46
|
+
model: model,
|
|
47
|
+
input: [
|
|
48
|
+
{
|
|
49
|
+
role: :system,
|
|
50
|
+
content: system_prompt
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
role: :user,
|
|
54
|
+
content: JSON.generate(context)
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
text: {
|
|
58
|
+
format: {
|
|
59
|
+
type: :json_schema,
|
|
60
|
+
name: "rails_proof_test_suggestions",
|
|
61
|
+
strict: true,
|
|
62
|
+
schema: RESPONSE_SCHEMA
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parse_suggestions(response.output_text)
|
|
68
|
+
rescue Error
|
|
69
|
+
raise
|
|
70
|
+
rescue StandardError => error
|
|
71
|
+
raise Error, "OpenAI request failed: #{error.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def client
|
|
77
|
+
@sdk_client ||= begin
|
|
78
|
+
require "openai"
|
|
79
|
+
|
|
80
|
+
OpenAI::Client.new
|
|
81
|
+
rescue LoadError
|
|
82
|
+
raise Error,
|
|
83
|
+
"OpenAI client is not installed. Add the openai gem to use AI planning."
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def system_prompt
|
|
88
|
+
<<~PROMPT
|
|
89
|
+
You are RailsProof's AI test planner and Minitest test generator.
|
|
90
|
+
|
|
91
|
+
Analyze Rails 8.1+ application code for meaningful Minitest coverage
|
|
92
|
+
that deterministic Rails inspection cannot already identify.
|
|
93
|
+
|
|
94
|
+
RailsProof has two kinds of AI-generated tests:
|
|
95
|
+
|
|
96
|
+
1. coverage
|
|
97
|
+
The implementation and apparent public contract agree. Generate a
|
|
98
|
+
test that verifies meaningful behavior implemented by the source.
|
|
99
|
+
|
|
100
|
+
2. contract_check
|
|
101
|
+
Strong evidence in the supplied source suggests that the apparent
|
|
102
|
+
public contract may disagree with the implementation. Generate a
|
|
103
|
+
test for the apparent intended contract, even when the current
|
|
104
|
+
implementation is likely to make that test fail.
|
|
105
|
+
|
|
106
|
+
Do not assume that the current implementation is correct merely
|
|
107
|
+
because it is the code that was supplied.
|
|
108
|
+
|
|
109
|
+
Look for strong local evidence of an implementation/contract mismatch,
|
|
110
|
+
including:
|
|
111
|
+
- method names whose semantics conflict with the implementation
|
|
112
|
+
- comments or surrounding code that contradict the implementation
|
|
113
|
+
- predicates whose implementation appears opposite to their name
|
|
114
|
+
- exact/equality semantics implemented as partial matching, or the
|
|
115
|
+
reverse
|
|
116
|
+
- state-changing method names whose implementation changes the wrong
|
|
117
|
+
state
|
|
118
|
+
- error-signaling method names whose implementation silently ignores
|
|
119
|
+
the error condition
|
|
120
|
+
- branches whose outcomes appear reversed relative to the public API
|
|
121
|
+
|
|
122
|
+
A contract_check must be supported by strong evidence in the supplied
|
|
123
|
+
source. Do not invent product requirements or speculate about behavior
|
|
124
|
+
when the intended contract is genuinely ambiguous.
|
|
125
|
+
|
|
126
|
+
If you identify behavior as a possible contract mismatch, do NOT also
|
|
127
|
+
generate a coverage test whose purpose is merely to lock that
|
|
128
|
+
suspicious implementation into the test suite.
|
|
129
|
+
|
|
130
|
+
Suggest only additional tests justified by the supplied source and
|
|
131
|
+
runtime context.
|
|
132
|
+
|
|
133
|
+
Do not duplicate deterministic concerns RailsProof already identified.
|
|
134
|
+
|
|
135
|
+
Do not suggest behavior that is already adequately covered by the
|
|
136
|
+
supplied existing tests.
|
|
137
|
+
|
|
138
|
+
Focus especially on:
|
|
139
|
+
- branches and conditional behavior
|
|
140
|
+
- custom public methods
|
|
141
|
+
- edge cases
|
|
142
|
+
- nil and blank handling
|
|
143
|
+
- state transitions
|
|
144
|
+
- persistence behavior
|
|
145
|
+
- side effects
|
|
146
|
+
- error conditions
|
|
147
|
+
- interactions between application objects
|
|
148
|
+
- possible implementation/contract disagreements
|
|
149
|
+
|
|
150
|
+
For every suggestion, generate the actual Minitest code needed to
|
|
151
|
+
exercise that behavior.
|
|
152
|
+
|
|
153
|
+
Each suggestion must have:
|
|
154
|
+
- kind: either "coverage" or "contract_check"
|
|
155
|
+
- name: a concise Minitest-style test description
|
|
156
|
+
- reason: why that behavior deserves a test
|
|
157
|
+
- test_code: exactly one complete Minitest test block
|
|
158
|
+
|
|
159
|
+
For a contract_check, the reason must explicitly describe:
|
|
160
|
+
- the evidence for the apparent contract
|
|
161
|
+
- how the implementation appears to disagree with that contract
|
|
162
|
+
|
|
163
|
+
test_code must:
|
|
164
|
+
- begin with a Minitest test declaration
|
|
165
|
+
- contain exactly one test block
|
|
166
|
+
- be valid Ruby
|
|
167
|
+
- contain no class declaration
|
|
168
|
+
- contain no require statement
|
|
169
|
+
- contain no Markdown code fences
|
|
170
|
+
- use only application behavior supported by the supplied context
|
|
171
|
+
- avoid assuming fixtures, factories, helpers, or methods not shown
|
|
172
|
+
in the supplied context
|
|
173
|
+
PROMPT
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def parse_suggestions(output_text)
|
|
177
|
+
parsed = JSON.parse(output_text)
|
|
178
|
+
suggestions = parsed.fetch("suggestions")
|
|
179
|
+
|
|
180
|
+
unless suggestions.is_a?(Array)
|
|
181
|
+
raise Error, "OpenAI response suggestions must be an array"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
suggestions
|
|
185
|
+
rescue JSON::ParserError, KeyError => error
|
|
186
|
+
raise Error, "Invalid OpenAI response: #{error.message}"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|