coatepec 0.5.1 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 161aafede82c7279701f75a51cc7976a6ef7bbbadff7bc22a7bc04dc050e1780
4
- data.tar.gz: 2b867df0d495bb573e1e00ad9a94f9aca800fa7045bbf6965fb0825acfc2683f
3
+ metadata.gz: ddcc044bcc53590ba19d988b0db8ea0aea4efd7db102bd6718196e61ab9ab820
4
+ data.tar.gz: 1b8b779b5bb59068a7fdd5fc90e7d44b900d50f1918e1d68877a9ccad24bf1e7
5
5
  SHA512:
6
- metadata.gz: d93777d12dac89c55450377259fc7d206bd6ef077766395f1397045372a58a7203bf986bbb9a525fc89b8510159a7fd9d0c3a057c321219edd95d6f23caa7acf
7
- data.tar.gz: 3e580288a7c4ef9b0e707909967c28293ce8bc8b749e8c11d03903faef69ef80587c4a8307e13e769633d521b6245a05f806859d124a40346a9a0120b56e3e9b
6
+ metadata.gz: 56fce0755730ab151c81fb43f88a28d6e081b242c3e9bb31fac71578c290e864efbc5e5313702a4c015b95a661cdd601f1701979c98dbbdae1a0cfa06e0be2fa
7
+ data.tar.gz: 4101b3595de972478ac414f3c019b413ec991cfe1d8ea0fbb251010fad6d699eaeb849818a1c4dbcfe813b47b41af220b68a7f971efe287dbbb8c598d8ff9763
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - Add `rails_spec_flaky_check`: runs a spec selection multiple times with
6
+ independently random seeds (RSpec's `--seed` is equivalent to `--order
7
+ rand:SEED`) and reports which examples' pass/fail status was
8
+ inconsistent across runs -- separately from examples that failed every
9
+ run (`consistently_failing`, not flaky) and examples that passed every
10
+ run (omitted). Each round's seed is included in the response so a
11
+ specific divergence can be reproduced with a plain `rails_spec_run`
12
+ call. `rails_spec_run` itself is unchanged by this release.
13
+
14
+ ## 0.5.2
15
+
16
+ - Fix `WorkerManager` getting permanently stuck treating a dead worker as
17
+ alive: `Worker::Client#alive?` used `Process.kill(0, pid)`, which returns
18
+ true for an unreaped zombie, and the one existing auto-restart-and-retry
19
+ path only caught `Worker::Client::DisconnectedError`, not a raw
20
+ `Errno::EPIPE`/`SystemCallError` from writing to a dead worker's pipe.
21
+ Every call after a worker died this way failed identically, forever --
22
+ since stdio MCP servers have no lighter reconnect in Claude Code, this
23
+ previously required a full session restart to recover from.
24
+ - Add `rails_runtime_restart`: unconditionally tears down and respawns the
25
+ test worker, for whatever the automatic detection above doesn't catch.
26
+ - Fix RSpec's own "(files took N seconds to load)" reporting growing
27
+ across every `rails_spec_run` call made through the same warm,
28
+ forked-per-run worker -- it was measuring time since the worker first
29
+ booted, not time this run's files took to load, because `rspec-core`
30
+ freezes that timestamp once per process and Coatepec never reset it on
31
+ reuse.
32
+
3
33
  ## 0.5.1
4
34
 
5
35
  - Fix `rails_model` crashing outright for a model with a `has_one`/
data/README.md CHANGED
@@ -127,8 +127,10 @@ a no-op.
127
127
  |---|---|---|
128
128
  | `rails_spec_run` | `paths: string[1..100]`, `example?`, `seed?`, `fail_fast?`, `timeout_seconds?` (1..900, default 120) | Isolated per run; output capped at 256 KiB per stream |
129
129
  | `rails_runtime_status` | `{}` | Reports Ruby/Rails versions, worker PID, boot_id, lifecycle state |
130
+ | `rails_runtime_restart` | `{}` | Unconditionally respawns the worker, discarding its warm boot |
130
131
  | `rails_routes` | `query?`, `limit?` (1..200, default 50), `offset?` | Case-insensitive filter across name/verb/path/controller/action |
131
132
  | `rails_model` | `name` (constant path, e.g. `Widget` or `Admin::Widget`) | ActiveRecord models only; columns, associations, validators, enums -- no row data |
133
+ | `rails_spec_flaky_check` | `paths`, `example?`, `timeout_seconds?` (per round, 1..900), `runs?` (2..20, default 5) | Runs the selection `runs` times with a fresh random seed each round; reports examples whose status was inconsistent across runs |
132
134
 
133
135
  ### Example queries
134
136
 
@@ -155,6 +157,30 @@ a no-op.
155
157
  rather than introspecting it (a nonexistent constant raises `model_not_found`
156
158
  instead) -- the tool only ever reflects on `ActiveRecord::Base` descendants.
157
159
 
160
+ `rails_spec_flaky_check`:
161
+
162
+ - "Is this spec flaky?" -- `rails_spec_flaky_check(paths: ["spec/models/widget_spec.rb"])`
163
+ runs it 5 times (default), each with an independently random seed
164
+ (equivalent to RSpec's `--order rand:SEED`), and reports any example
165
+ whose pass/fail status wasn't the same every time under `flaky_examples`
166
+ -- distinct from `consistently_failing` (fails every run: broken, not
167
+ flaky) and examples that passed every run (omitted -- nothing to report).
168
+ - Each round's seed is included in the response (`rounds[].seed`), so a
169
+ specific divergence can be reproduced afterward with a plain
170
+ `rails_spec_run(seed: <that seed>)`.
171
+ - `timeout_seconds` is a **per-round** budget, not a total; `runs *
172
+ timeout_seconds` is capped at 1800s combined (`flaky_check_budget_exceeded`
173
+ if exceeded) since this tool can run for a while.
174
+ - Selections over 500 examples are capped per round (the same limit
175
+ `rails_spec_run` already has), and each round samples a different subset,
176
+ since execution order varies by design -- for suites this large, narrow
177
+ `paths`/`example` rather than passing a very broad directory selection.
178
+ - `statuses[]` only aligns positionally with `rounds[]` when no round
179
+ crashed -- a round whose process itself failed contributes no entry to
180
+ `statuses[]` (though it still appears in `rounds[]`), so treat positional
181
+ correspondence as best-effort, not guaranteed, when a round's `status` in
182
+ `rounds[]` looks like an outright crash rather than a normal pass/fail.
183
+
158
184
  The warm test worker forces Rails' reload-checking on for its own boot,
159
185
  regardless of the target app's own `test.rb` setting (which disables it by
160
186
  default) -- so editing a model file takes effect on the next tool call
@@ -178,6 +204,13 @@ across a commit that touches the Gemfile, since the sidecar is managed by
178
204
  your MCP client rather than by Coatepec itself: restart your MCP client (or
179
205
  however it manages the Coatepec process) to pick up the change.
180
206
 
207
+ A dead *worker* (as opposed to a dead sidecar) recovers on its own: the
208
+ next tool call detects it and transparently boots a fresh one before
209
+ retrying, whether the worker exited outright or a request to it failed
210
+ with a broken pipe. If you want a fresh worker without waiting for a
211
+ failure -- or one keeps recurring -- call `rails_runtime_restart` directly;
212
+ it always respawns, even if the current worker looks healthy.
213
+
181
214
  ## Security boundary
182
215
 
183
216
  No eval, console, SQL/record access, shell, Rake, or file-write tool. Spec
@@ -69,6 +69,72 @@ module Coatepec
69
69
  end
70
70
  end
71
71
 
72
+ # The `rails_runtime_restart` MCP tool: unconditionally tears down and
73
+ # respawns the test worker, discarding its warm Rails boot. A manual
74
+ # escape hatch alongside WorkerManager's own automatic dead-worker
75
+ # detection, for whatever failure mode that detection doesn't catch.
76
+ class RuntimeRestartTool < ::MCP::Tool
77
+ tool_name "rails_runtime_restart"
78
+ description "Tear down and respawn the Coatepec test worker, discarding its warm Rails boot " \
79
+ "(use if rails_spec_run/rails_runtime_status keep failing and a fresh worker is needed)"
80
+ annotations(read_only_hint: false, destructive_hint: true, idempotent_hint: false, open_world_hint: false)
81
+ input_schema(properties: {}, required: [], additionalProperties: false)
82
+
83
+ class << self
84
+ def call(server_context:)
85
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
86
+ data = server_context[:worker_manager].restart!
87
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
88
+ Response.ok(data: data,
89
+ meta: { project_root: server_context[:project_root], environment: "test",
90
+ duration_ms: duration_ms })
91
+ rescue Coatepec::Error => e
92
+ Response.error(e)
93
+ end
94
+ end
95
+ end
96
+
97
+ # The `rails_spec_flaky_check` MCP tool: runs targeted RSpec examples
98
+ # multiple times with independently random seeds and reports which
99
+ # examples' pass/fail status was inconsistent across rounds. Separate
100
+ # tool from rails_spec_run for the same reason rails_spec_profile is --
101
+ # see docs/superpowers/specs/2026-08-13-flaky-spec-detection-design.md.
102
+ class FlakyCheckTool < ::MCP::Tool
103
+ tool_name "rails_spec_flaky_check"
104
+ description "Run targeted RSpec examples multiple times with random seeds to detect order-dependent or " \
105
+ "intermittent flakiness, reporting which examples' status was inconsistent across runs"
106
+ annotations(read_only_hint: false, destructive_hint: true, idempotent_hint: false, open_world_hint: true)
107
+ input_schema(
108
+ properties: {
109
+ paths: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 100 },
110
+ example: { type: %w[string null] },
111
+ timeout_seconds: { type: "integer", minimum: 1, maximum: 900 },
112
+ runs: { type: "integer", minimum: 2, maximum: 20 }
113
+ },
114
+ required: ["paths"],
115
+ additionalProperties: false
116
+ )
117
+
118
+ class << self
119
+ def call(paths:, server_context:, example: nil, timeout_seconds: 120, runs: 5)
120
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
121
+ data = server_context[:worker_manager].check_flaky(
122
+ paths: paths, example: example, timeout_seconds: timeout_seconds, runs: runs
123
+ )
124
+ Response.ok(data: data, meta: meta_for(server_context, started_at))
125
+ rescue Coatepec::Error => e
126
+ Response.error(e)
127
+ end
128
+
129
+ private
130
+
131
+ def meta_for(server_context, started_at)
132
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
133
+ { project_root: server_context[:project_root], environment: "test", duration_ms: duration_ms }
134
+ end
135
+ end
136
+ end
137
+
72
138
  # The `rails_routes` MCP tool: lists/filters/paginates the target
73
139
  # Rails app's routes.
74
140
  class RoutesTool < ::MCP::Tool
data/lib/coatepec/mcp.rb CHANGED
@@ -12,15 +12,15 @@ require_relative "mcp/response"
12
12
  require_relative "mcp/tools"
13
13
 
14
14
  module Coatepec
15
- # Wires the `rails_spec_run`, `rails_runtime_status`, `rails_routes`, and
16
- # `rails_model` tools into an `::MCP::Server` instance backed by the given
17
- # project's worker manager.
15
+ # Wires the `rails_spec_run`, `rails_runtime_status`, `rails_runtime_restart`,
16
+ # `rails_spec_flaky_check`, `rails_routes`, and `rails_model` tools into an
17
+ # `::MCP::Server` instance backed by the given project's worker manager.
18
18
  module MCP
19
19
  def self.build_server(project:, worker_manager:)
20
20
  ::MCP::Server.new(
21
21
  name: "coatepec",
22
22
  version: Coatepec::VERSION,
23
- tools: [SpecRunTool, RuntimeStatusTool, RoutesTool, ModelTool],
23
+ tools: [SpecRunTool, RuntimeStatusTool, RuntimeRestartTool, FlakyCheckTool, RoutesTool, ModelTool],
24
24
  server_context: { worker_manager: worker_manager, project_root: project.root }
25
25
  )
26
26
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Coatepec
6
+ module Spec
7
+ # Runs a spec selection multiple times, each round with an
8
+ # independently random seed (RSpec's --seed N is equivalent to --order
9
+ # rand:N, confirmed against rspec-core's own option_parser.rb), and
10
+ # groups per-example pass/fail status across rounds by the example's
11
+ # own id -- stable regardless of execution order, since it's derived
12
+ # from declaration position/nesting, not from --order. Composes Runner
13
+ # unchanged, exactly like FactoryProfRunner: every round is a plain,
14
+ # ordinary Runner#run call with a different seed, no strategy override
15
+ # needed (unlike FactoryProfRunner, this feature has no load-time
16
+ # activation trick to fight -- see
17
+ # docs/superpowers/specs/2026-08-13-flaky-spec-detection-design.md).
18
+ class FlakyChecker
19
+ DEFAULT_RUNS = 5
20
+ MAX_TOTAL_SECONDS = 1800 # timeout_seconds * runs must not exceed this
21
+ MAX_ITEMS = 200 # same bound rails_model/FactoryProfRunner use elsewhere
22
+
23
+ def initialize(project_root, rails_runtime: nil)
24
+ @runner = Runner.new(project_root, rails_runtime: rails_runtime)
25
+ end
26
+
27
+ def call(paths:, example: nil, timeout_seconds: Runner::DEFAULT_TIMEOUT, runs: DEFAULT_RUNS)
28
+ validate_budget!(timeout_seconds, runs)
29
+ rounds = Array.new(runs) { run_one_round(paths, example, timeout_seconds) }
30
+ build_report(rounds)
31
+ end
32
+
33
+ private
34
+
35
+ def validate_budget!(timeout_seconds, runs)
36
+ return if timeout_seconds * runs <= MAX_TOTAL_SECONDS
37
+
38
+ raise Coatepec::Error.new(
39
+ :flaky_check_budget_exceeded,
40
+ "timeout_seconds (#{timeout_seconds}) * runs (#{runs}) exceeds the " \
41
+ "#{MAX_TOTAL_SECONDS}s combined budget -- lower one or both"
42
+ )
43
+ end
44
+
45
+ def run_one_round(paths, example, timeout_seconds)
46
+ seed = SecureRandom.random_number(65_536)
47
+ result = @runner.run(
48
+ paths: paths, example: example, seed: seed, fail_fast: false,
49
+ timeout_seconds: timeout_seconds
50
+ )
51
+ { seed: seed, status: result[:status], examples: result[:examples] }
52
+ end
53
+
54
+ def build_report(rounds)
55
+ by_id = group_examples_by_id(rounds)
56
+ {
57
+ runs: rounds.size,
58
+ rounds: rounds.map { |r| { seed: r[:seed], status: r[:status] } },
59
+ flaky_examples: classify(by_id) { |statuses| statuses.uniq.size > 1 },
60
+ consistently_failing: classify(by_id) { |statuses| statuses.uniq == ["failed"] }
61
+ }
62
+ end
63
+
64
+ # Rounds whose RSpec process itself crashed/timed out (not a specific
65
+ # example failing -- the whole run killed before its JSON summary was
66
+ # ever written) contribute no per-example data here. Excluded rather
67
+ # than counted as "every example failed", which would misrepresent a
68
+ # dead process as evidence against examples that never actually ran;
69
+ # the round itself is still visible in the `rounds:` summary.
70
+ def group_examples_by_id(rounds)
71
+ rounds.each_with_object({}) do |round, acc|
72
+ round[:examples].each { |ex| (acc[ex[:id]] ||= { meta: ex, statuses: [] })[:statuses] << ex[:status] }
73
+ end
74
+ end
75
+
76
+ def classify(by_id)
77
+ by_id.values.select { |v| yield(v[:statuses]) }.first(MAX_ITEMS).map do |v|
78
+ v[:meta].slice(:id, :description, :file_path, :line_number).merge(
79
+ statuses: v[:statuses],
80
+ pass_count: v[:statuses].count("passed"),
81
+ failure_count: v[:statuses].count("failed")
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -14,6 +14,15 @@ module Coatepec
14
14
  redirect_output(out_w, err_w)
15
15
  # Forked children must not share the parent's live DB sockets.
16
16
  ActiveRecord::Base.connection_handler.clear_all_connections! if defined?(ActiveRecord::Base)
17
+ # RSpec freezes its own "load started at" timestamp once, at the moment
18
+ # rspec/core.rb is first required -- in this architecture, that's when
19
+ # the long-lived warm worker booted, not when THIS run started. Every
20
+ # forked child inherits that frozen timestamp via copy-on-write, so
21
+ # RSpec's own "(files took N seconds to load)" reporting would
22
+ # otherwise measure "time since the worker booted" and grow across
23
+ # every run for as long as the worker stays warm. Reset it fresh
24
+ # before each run.
25
+ RSpec.configuration.start_time = RSpec::Core::Time.now
17
26
 
18
27
  status = RSpec::Core::Runner.run(full_args, $stderr, $stdout)
19
28
  $stdout.flush
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coatepec
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -32,9 +32,20 @@ module Coatepec
32
32
  def alive?
33
33
  return false unless @pid
34
34
 
35
+ # A dead-but-unreaped worker (crash, the OS reclaiming a long-idle
36
+ # process, anything) is a zombie: Process.kill(0, pid) below would still
37
+ # succeed against it, since it still holds a process-table entry. Reap it
38
+ # here so staleness is detected instead of reported as "alive" forever --
39
+ # nothing else calls Process.wait on this pid except #stop, which only
40
+ # WorkerManager#restart_worker! reaches, and only once #alive? itself
41
+ # already says false.
42
+ _pid, status = Process.waitpid2(@pid, Process::WNOHANG)
43
+ @pid = nil if status
44
+ return false unless @pid
45
+
35
46
  Process.kill(0, @pid)
36
47
  true
37
- rescue Errno::ESRCH
48
+ rescue Errno::ESRCH, Errno::ECHILD
38
49
  false
39
50
  end
40
51
 
@@ -42,6 +42,7 @@ module Coatepec
42
42
  case command
43
43
  when "status" then @runtime.status
44
44
  when "spec_run" then handle_spec_run(args)
45
+ when "flaky_check" then handle_flaky_check(args)
45
46
  when "routes" then handle_routes(args)
46
47
  when "model" then handle_model(args)
47
48
  else
@@ -53,6 +54,10 @@ module Coatepec
53
54
  Spec::Runner.new(@project_root, rails_runtime: @runtime).run(**args.transform_keys(&:to_sym))
54
55
  end
55
56
 
57
+ def handle_flaky_check(args)
58
+ Spec::FlakyChecker.new(@project_root, rails_runtime: @runtime).call(**args.transform_keys(&:to_sym))
59
+ end
60
+
56
61
  def handle_routes(args)
57
62
  Introspection::Routes.new(**args.transform_keys(&:to_sym)).call
58
63
  end
@@ -28,6 +28,18 @@ module Coatepec
28
28
  )
29
29
  end
30
30
 
31
+ def check_flaky(paths:, example:, timeout_seconds:, runs:)
32
+ dispatch(
33
+ "flaky_check",
34
+ { paths: paths, example: example, timeout_seconds: timeout_seconds, runs: runs },
35
+ # Each round costs timeout_seconds *plus* real per-round overhead (process
36
+ # termination/spawn/result-parsing -- roughly 0.6s+ per round from
37
+ # ProcessStrategy#terminate's own escalation sleeps alone), so the flat
38
+ # base slack below is scaled by an extra ~2s per round on top of it.
39
+ timeout: (timeout_seconds * runs) + (2 * runs) + 10
40
+ )
41
+ end
42
+
31
43
  def routes(query: nil, limit: 50, offset: 0)
32
44
  dispatch("routes", { query: query, limit: limit, offset: offset }, timeout: 30)
33
45
  end
@@ -40,6 +52,27 @@ module Coatepec
40
52
  @lock.synchronize { @client&.stop }
41
53
  end
42
54
 
55
+ # Unconditionally tears down and respawns the worker, bypassing the usual
56
+ # "only restart if #ensure_worker! thinks it's needed" check. Manual escape
57
+ # hatch for rails_runtime_restart, independent of whatever #perform's
58
+ # rescue below already recovers from automatically.
59
+ #
60
+ # Still honors the sidecar-restart invariant: if the Gemfile changed since
61
+ # boot, a worker-only respawn would silently adopt the new bundle in the
62
+ # worker while the parent MCP process stays on the old one, so that case
63
+ # raises :sidecar_restart_required instead of restarting. The check runs
64
+ # against the *pre-existing* snapshot, before restart_worker! re-baselines
65
+ # it -- same ordering #dispatch uses. Nothing to compare against on the
66
+ # very first call.
67
+ def restart!
68
+ @lock.synchronize do
69
+ raise_sidecar_restart_required! if sidecar_restart_required?
70
+
71
+ restart_worker!
72
+ status
73
+ end
74
+ end
75
+
43
76
  private
44
77
 
45
78
  def dispatch(command, args, timeout:, retried: false)
@@ -57,26 +90,33 @@ module Coatepec
57
90
 
58
91
  def perform(command, args, timeout:, retried:)
59
92
  @client.request(command, args, timeout: timeout)
60
- rescue Worker::Client::DisconnectedError
61
- raise Coatepec::Error.new(:worker_disconnected, "Worker disconnected") if retried
93
+ rescue Worker::Client::DisconnectedError, SystemCallError, IOError => e
94
+ raise Coatepec::Error.new(:worker_disconnected, "Worker disconnected: #{e.message}") if retried
62
95
 
63
96
  restart_worker!
64
97
  dispatch(command, args, timeout: timeout, retried: true)
65
98
  end
66
99
 
67
100
  def check_for_restart!
68
- reason = @change_detector.restart_reason(@snapshot)
69
- case reason
101
+ case @change_detector.restart_reason(@snapshot)
70
102
  when :sidecar_restart_required
71
- raise Coatepec::Error.new(
72
- :sidecar_restart_required,
73
- "Gemfile changed; restart your MCP client to restart Coatepec and pick up the change"
74
- )
103
+ raise_sidecar_restart_required!
75
104
  when :worker_restart_required
76
105
  restart_worker!
77
106
  end
78
107
  end
79
108
 
109
+ def sidecar_restart_required?
110
+ @snapshot && @change_detector.restart_reason(@snapshot) == :sidecar_restart_required
111
+ end
112
+
113
+ def raise_sidecar_restart_required!
114
+ raise Coatepec::Error.new(
115
+ :sidecar_restart_required,
116
+ "Gemfile changed; restart your MCP client to restart Coatepec and pick up the change"
117
+ )
118
+ end
119
+
80
120
  def ensure_worker!
81
121
  restart_worker! unless @client&.alive?
82
122
  end
data/lib/coatepec.rb CHANGED
@@ -18,6 +18,7 @@ require_relative "coatepec/spec/fork_strategy"
18
18
  require_relative "coatepec/spec/spawn_strategy"
19
19
  require_relative "coatepec/spec/guarded_fork_strategy"
20
20
  require_relative "coatepec/spec/runner"
21
+ require_relative "coatepec/spec/flaky_checker"
21
22
  require_relative "coatepec/worker/server"
22
23
  require_relative "coatepec/worker_manager"
23
24
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coatepec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Enrique Mogollan
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-07 00:00:00.000000000 Z
10
+ date: 2026-08-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: railties
@@ -74,6 +74,7 @@ files:
74
74
  - lib/coatepec/project.rb
75
75
  - lib/coatepec/project_config.rb
76
76
  - lib/coatepec/protocol.rb
77
+ - lib/coatepec/spec/flaky_checker.rb
77
78
  - lib/coatepec/spec/fork_strategy.rb
78
79
  - lib/coatepec/spec/guarded_fork_strategy.rb
79
80
  - lib/coatepec/spec/path_policy.rb