ruby_raider 4.1.0 → 4.2.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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +3 -0
  3. data/README.md +52 -53
  4. data/RELEASE.md +14 -1
  5. data/lib/commands/schedule_commands.rb +84 -0
  6. data/lib/generators/templates/common/git_ignore.tt +1 -0
  7. data/lib/ruby_raider/runtime/retry_reset.rb +32 -0
  8. data/lib/ruby_raider/runtime/rspec.rb +1 -0
  9. data/lib/ruby_raider/runtime/show_browser.rb +53 -0
  10. data/lib/ruby_raider/runtime.rb +2 -0
  11. data/lib/ruby_raider.rb +22 -7
  12. data/lib/utilities/schedules/adapter.rb +22 -0
  13. data/lib/utilities/schedules/base.rb +27 -0
  14. data/lib/utilities/schedules/kind.rb +74 -0
  15. data/lib/utilities/schedules/legacy.rb +196 -0
  16. data/lib/utilities/schedules/legacy_stub.rb +62 -0
  17. data/lib/utilities/{flows → schedules}/next_run.rb +2 -2
  18. data/lib/utilities/{flows → schedules}/notifier.rb +6 -6
  19. data/lib/utilities/{flows → schedules}/os/command_runner.rb +1 -1
  20. data/lib/utilities/{flows → schedules}/os/cron.rb +27 -17
  21. data/lib/utilities/{flows → schedules}/os/launchd.rb +32 -14
  22. data/lib/utilities/{flows → schedules}/os/linux.rb +23 -5
  23. data/lib/utilities/{flows → schedules}/os/systemd.rb +33 -17
  24. data/lib/utilities/{flows → schedules}/os/task_scheduler.rb +36 -20
  25. data/lib/utilities/schedules/os_enumerator.rb +30 -0
  26. data/lib/utilities/schedules/project.rb +70 -0
  27. data/lib/utilities/schedules/rule.rb +162 -0
  28. data/lib/utilities/schedules/run_lock.rb +160 -0
  29. data/lib/utilities/schedules/run_log.rb +122 -0
  30. data/lib/utilities/schedules/run_report.rb +139 -0
  31. data/lib/utilities/schedules/run_wrapper.rb +216 -0
  32. data/lib/utilities/schedules/schedule.rb +89 -0
  33. data/lib/utilities/schedules/schedule_store.rb +53 -0
  34. data/lib/utilities/schedules/scheduler.rb +178 -0
  35. data/lib/utilities/schedules/scheduler_steps.rb +69 -0
  36. data/lib/utilities/schedules/selection.rb +133 -0
  37. data/lib/utilities/schedules/spawn.rb +68 -0
  38. data/lib/utilities/{flows/schedule_store.rb → schedules/store.rb} +17 -13
  39. data/lib/version +1 -1
  40. data/ruby_raider.gemspec +1 -0
  41. data/sig/commands/schedule_commands.rbs +12 -0
  42. data/sig/ruby_raider/runtime/retry_reset.rbs +7 -0
  43. data/sig/ruby_raider/runtime/show_browser.rbs +27 -0
  44. data/sig/ruby_raider.rbs +4 -0
  45. data/sig/utilities/schedules/adapter.rbs +5 -0
  46. data/sig/utilities/schedules/base.rbs +15 -0
  47. data/sig/utilities/schedules/kind.rbs +32 -0
  48. data/sig/utilities/schedules/legacy.rbs +45 -0
  49. data/sig/utilities/schedules/legacy_stub.rbs +15 -0
  50. data/sig/utilities/{flows → schedules}/next_run.rbs +3 -3
  51. data/sig/utilities/{flows → schedules}/notifier.rbs +1 -1
  52. data/sig/utilities/{flows → schedules}/os/command_runner.rbs +1 -1
  53. data/sig/utilities/{flows → schedules}/os/cron.rbs +7 -7
  54. data/sig/utilities/{flows → schedules}/os/launchd.rbs +7 -6
  55. data/sig/utilities/{flows → schedules}/os/linux.rbs +10 -4
  56. data/sig/utilities/{flows → schedules}/os/systemd.rbs +8 -7
  57. data/sig/utilities/{flows → schedules}/os/task_scheduler.rbs +13 -10
  58. data/sig/utilities/schedules/os_enumerator.rbs +12 -0
  59. data/sig/utilities/schedules/project.rbs +22 -0
  60. data/sig/utilities/{flows/schedule.rbs → schedules/rule.rbs} +23 -12
  61. data/sig/utilities/schedules/run_lock.rbs +29 -0
  62. data/sig/utilities/schedules/run_log.rbs +33 -0
  63. data/sig/utilities/schedules/run_report.rbs +34 -0
  64. data/sig/utilities/schedules/run_wrapper.rbs +45 -0
  65. data/sig/utilities/schedules/schedule.rbs +27 -0
  66. data/sig/utilities/schedules/schedule_store.rbs +21 -0
  67. data/sig/utilities/{flows → schedules}/scheduler.rbs +22 -15
  68. data/sig/utilities/schedules/scheduler_steps.rbs +22 -0
  69. data/sig/utilities/schedules/selection.rbs +36 -0
  70. data/sig/utilities/schedules/spawn.rbs +10 -0
  71. data/sig/utilities/{flows/schedule_store.rbs → schedules/store.rbs} +14 -9
  72. data/spec/commands/schedule_commands_spec.rb +284 -0
  73. data/spec/integration/content/common_content_spec.rb +8 -2
  74. data/spec/integration/content/gemfile_content_spec.rb +2 -2
  75. data/spec/integration/end_to_end_spec.rb +0 -63
  76. data/spec/runtime/retry_reset_spec.rb +112 -0
  77. data/spec/runtime/show_browser_project_spec.rb +70 -0
  78. data/spec/runtime/show_browser_spec.rb +101 -0
  79. data/spec/utilities/schedules/kind_spec.rb +81 -0
  80. data/spec/utilities/schedules/legacy_spec.rb +433 -0
  81. data/spec/utilities/schedules/legacy_stub_spec.rb +144 -0
  82. data/spec/utilities/{flows → schedules}/next_run_spec.rb +5 -5
  83. data/spec/utilities/{flows → schedules}/notifier_spec.rb +3 -4
  84. data/spec/utilities/{flows → schedules}/os/command_runner_spec.rb +2 -3
  85. data/spec/utilities/{flows → schedules}/os/cron_spec.rb +34 -29
  86. data/spec/utilities/{flows → schedules}/os/launchd_spec.rb +42 -29
  87. data/spec/utilities/{flows → schedules}/os/linux_spec.rb +27 -13
  88. data/spec/utilities/schedules/os/list_ids_spec.rb +202 -0
  89. data/spec/utilities/{flows → schedules}/os/systemd_spec.rb +35 -31
  90. data/spec/utilities/{flows → schedules}/os/task_scheduler_spec.rb +40 -34
  91. data/spec/utilities/schedules/os_entry_spec.rb +54 -0
  92. data/spec/utilities/schedules/os_enumerator_spec.rb +57 -0
  93. data/spec/utilities/schedules/project_spec.rb +125 -0
  94. data/spec/utilities/schedules/run_lock_spec.rb +143 -0
  95. data/spec/utilities/schedules/run_log_spec.rb +83 -0
  96. data/spec/utilities/schedules/run_report_spec.rb +150 -0
  97. data/spec/utilities/schedules/run_wrapper_integration_spec.rb +166 -0
  98. data/spec/utilities/schedules/run_wrapper_spec.rb +430 -0
  99. data/spec/utilities/schedules/schedule_spec.rb +120 -0
  100. data/spec/utilities/schedules/schedule_store_spec.rb +151 -0
  101. data/spec/utilities/schedules/scheduler_launchd_spec.rb +77 -0
  102. data/spec/utilities/schedules/scheduler_spec.rb +445 -0
  103. data/spec/utilities/schedules/selection_spec.rb +176 -0
  104. data/spec/utilities/schedules/spawn_spec.rb +104 -0
  105. metadata +106 -53
  106. data/lib/commands/flow_commands.rb +0 -104
  107. data/lib/generators/templates/flows/flow_helper.tt +0 -42
  108. data/lib/generators/templates/flows/run.tt +0 -332
  109. data/lib/utilities/flows/project.rb +0 -170
  110. data/lib/utilities/flows/run_log.rb +0 -61
  111. data/lib/utilities/flows/runner.rb +0 -206
  112. data/lib/utilities/flows/schedule.rb +0 -221
  113. data/lib/utilities/flows/scheduler.rb +0 -187
  114. data/sig/commands/flow_commands.rbs +0 -13
  115. data/sig/utilities/flows/project.rbs +0 -46
  116. data/sig/utilities/flows/run_log.rbs +0 -22
  117. data/sig/utilities/flows/runner.rbs +0 -35
  118. data/spec/commands/flow_commands_spec.rb +0 -128
  119. data/spec/generators/flow_helper_spec.rb +0 -40
  120. data/spec/integration/flows/flow_project.rb +0 -62
  121. data/spec/integration/flows/run_command_spec.rb +0 -91
  122. data/spec/integration/flows/run_lifecycle_spec.rb +0 -383
  123. data/spec/utilities/flows/project_spec.rb +0 -178
  124. data/spec/utilities/flows/run_log_spec.rb +0 -47
  125. data/spec/utilities/flows/runner_spec.rb +0 -313
  126. data/spec/utilities/flows/schedule_spec.rb +0 -233
  127. data/spec/utilities/flows/schedule_store_spec.rb +0 -137
  128. data/spec/utilities/flows/scheduler_spec.rb +0 -329
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+ require_relative '../../../../lib/utilities/schedules/adapter'
5
+ require_relative '../../../../lib/utilities/schedules/schedule'
6
+
7
+ # Every adapter's list_ids(prefix): the os_ids of one project's schedules of the adapter's Kind, found by
8
+ # enumerating the OS scheduler. Only a fake runner and a temp home: nothing real is read or run.
9
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
10
+ RSpec.describe 'Schedules adapters #list_ids' do
11
+ # Answers each command from a table keyed by its first words; never runs anything
12
+ let(:fake_runner_class) do
13
+ Class.new do
14
+ attr_reader :calls, :answers
15
+
16
+ def initialize
17
+ @calls = []
18
+ @answers = {}
19
+ end
20
+
21
+ def call(*argv, stdin_data: nil)
22
+ @calls << [argv, stdin_data]
23
+ key = if argv.first.to_s.end_with?('schtasks.exe') then argv[1]
24
+ elsif argv.first == 'systemctl' then argv.first(3).join(' ')
25
+ else argv.first(2).join(' ')
26
+ end
27
+ stdout, status, stderr = answers.fetch(key, ['', 0, ''])
28
+ RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: stdout, stderr: stderr.to_s, exitstatus: status)
29
+ end
30
+ end
31
+ end
32
+
33
+ let(:kind) { RubyRaider::Schedules::Kind::TESTS }
34
+ let(:tmp) { File.realpath(Dir.mktmpdir('schedules-list-ids')) }
35
+ let(:home) { File.join(tmp, 'home') }
36
+ let(:runner) { fake_runner_class.new }
37
+ let(:root) { File.join(tmp, 'project') }
38
+ let(:prefix) { RubyRaider::Schedules::Schedule.os_id_prefix_for(root) }
39
+ let(:other_prefix) { RubyRaider::Schedules::Schedule.os_id_prefix_for(File.join(tmp, 'other')) }
40
+ let(:flow_prefix) { "#{RubyRaider::Schedules::Kind::FLOW_LEGACY.os_id_prefix}.#{prefix.split('.')[3]}." }
41
+ let(:bad_utf8) { "#{prefix}a\xFF".dup.force_encoding(Encoding::UTF_8) }
42
+ # What enumeration may see besides our two ids: another project, a flow schedule, a bad name, a longer
43
+ # name, path tricks and bytes that are not UTF-8
44
+ let(:noise) do
45
+ ["#{other_prefix}nightly", "#{flow_prefix}nightly", "#{prefix}Bad", "#{prefix}#{'a' * 61}", "#{prefix}a.b", "#{prefix}..",
46
+ "#{prefix}a b", prefix, "x#{prefix}nightly", bad_utf8]
47
+ end
48
+
49
+ let(:extra_children) { Hash.new { |hash, key| hash[key] = [] } }
50
+
51
+ after { FileUtils.rm_rf(tmp) }
52
+
53
+ # APFS refuses a file name that is not UTF-8, so those names are fed through Dir.children instead
54
+ def touch(dir, *names)
55
+ FileUtils.mkdir_p(dir)
56
+ names.each { |name| name.valid_encoding? ? File.write(File.join(dir, name), '') : extra_children[dir] << name }
57
+ end
58
+
59
+ before do
60
+ allow(Dir).to(receive(:children).and_wrap_original { |original, dir| original.call(dir) + extra_children[dir] })
61
+ end
62
+
63
+ it 'builds the prefix from the kind and the project path hash, ending in a dot' do
64
+ expect(prefix).to match(/\Adev\.rubyraider\.schedule\.[0-9a-f]{10}\.\z/)
65
+ expect(RubyRaider::Schedules::Schedule.os_id_for(root, 'nightly')).to eq("#{prefix}nightly")
66
+ end
67
+
68
+ describe RubyRaider::Schedules::Os::Launchd do
69
+ let(:adapter) { described_class.new(kind: kind, runner: runner, home: home, uid: 501) }
70
+ let(:agents) { File.join(home, 'Library', 'LaunchAgents') }
71
+
72
+ it 'lists the plist basenames of this project and kind, sorted, and runs nothing' do
73
+ touch(agents, "#{prefix}smoke.plist", "#{prefix}nightly.plist", "#{prefix}nightly.txt", ".#{prefix}x.plist.ab12.tmp",
74
+ *noise.map { |id| "#{id}.plist" })
75
+
76
+ expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
77
+ expect([runner.calls, extra_children[agents]]).to eq([[], ["#{bad_utf8}.plist"]])
78
+ end
79
+
80
+ it 'is empty when there is no LaunchAgents folder' do
81
+ expect(adapter.list_ids(prefix)).to eq([])
82
+ end
83
+
84
+ it 'raises an Error when the folder can not be read' do
85
+ FileUtils.mkdir_p(File.join(home, 'Library'))
86
+ File.write(agents, '')
87
+
88
+ expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, /Could not read .*LaunchAgents/)
89
+ end
90
+ end
91
+
92
+ describe RubyRaider::Schedules::Os::TaskScheduler do
93
+ let(:adapter) { described_class.new(kind: kind, runner: runner, schtasks: 'C:\\Windows\\System32\\schtasks.exe') }
94
+
95
+ def rows(*names) = names.map { |name| %("#{name}","11/10/2026 8:00:00 AM","Ready"\r\n) }.join
96
+
97
+ it 'lists the tasks under \\RubyRaider\\ of this project and kind from one query' do
98
+ listing = rows("\\RubyRaider\\#{prefix}smoke", "\\RubyRaider\\#{prefix}nightly", "\\Other\\#{prefix}elsewhere",
99
+ "\\RubyRaider\\sub\\#{prefix}deeper", "\\#{prefix}rootlevel", "\\RubyRaider\\#{prefix}smoke",
100
+ *noise.map { |id| "\\RubyRaider\\#{id}" })
101
+ listing = "#{listing}garbage line\r\n\"unterminated\r\n"
102
+ runner.answers['/Query'] = [listing.b, 0]
103
+
104
+ expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
105
+ expect(runner.calls.map(&:first)).to eq([['C:\\Windows\\System32\\schtasks.exe', '/Query', '/FO', 'CSV', '/NH']])
106
+ end
107
+
108
+ it 'raises an Error when the task list can not be read' do
109
+ runner.answers['/Query'] = ['', 1, "ERROR: \e[2Jdenied"]
110
+
111
+ expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, 'Task Scheduler could not list tasks: ERROR: ?[2Jdenied')
112
+ end
113
+ end
114
+
115
+ describe RubyRaider::Schedules::Os::Systemd do
116
+ let(:adapter) { described_class.new(kind: kind, runner: runner, home: home, config_home: nil) }
117
+ let(:units) { File.join(home, '.config', 'systemd', 'user') }
118
+
119
+ it 'lists the unit file names of this project and kind, without asking systemctl' do
120
+ touch(units, "#{prefix}smoke.timer", "#{prefix}smoke.service", "#{prefix}nightly.service", "#{prefix}other.socket",
121
+ ".#{prefix}x.timer.ab12.tmp", *noise.map { |id| "#{id}.timer" })
122
+
123
+ expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
124
+ expect(runner.calls).to be_empty
125
+ end
126
+
127
+ it 'reads the units folder under XDG_CONFIG_HOME when that is absolute' do
128
+ config = File.join(tmp, 'xdg')
129
+ touch(File.join(config, 'systemd', 'user'), "#{prefix}smoke.timer")
130
+
131
+ expect(described_class.new(kind: kind, runner: runner, home: home, config_home: config).list_ids(prefix)).to eq(["#{prefix}smoke"])
132
+ end
133
+
134
+ it 'is empty when there is no units folder' do
135
+ expect(adapter.list_ids(prefix)).to eq([])
136
+ end
137
+ end
138
+
139
+ describe RubyRaider::Schedules::Os::Cron do
140
+ let(:adapter) { described_class.new(kind: kind, runner: runner) }
141
+
142
+ it 'lists the ids on our tag lines only, keeping the crontab as it is' do
143
+ table = "MAILTO=x\n# raider-schedule #{prefix}smoke\n0 8 * * * cd /p && run\n# raider-schedule #{prefix}nightly\r\n" \
144
+ "# raider-schedule #{prefix}spaced\n# raider-schedule #{prefix}trailing \n" \
145
+ "0 1 * * * echo '# raider-schedule #{prefix}inline'\n" \
146
+ "#{noise.map { |id| "# raider-schedule #{id}\n" }.join}# raider-flow #{flow_prefix}legacy\n"
147
+ runner.answers['crontab -l'] = [table.b, 0]
148
+
149
+ expect(adapter.list_ids(prefix)).to eq(["#{prefix}nightly", "#{prefix}smoke"])
150
+ expect(runner.calls.map(&:first)).to eq([%w[crontab -l]])
151
+ end
152
+
153
+ it 'is empty with no crontab, and raises an Error when crontab -l fails otherwise' do
154
+ runner.answers['crontab -l'] = ['', 1, 'no crontab for raider']
155
+ expect(adapter.list_ids(prefix)).to eq([])
156
+
157
+ runner.answers['crontab -l'] = ['', 1, 'permission denied']
158
+ expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, /could not read the crontab/)
159
+ end
160
+
161
+ it 'raises an Error, not an encoding error, when crontab -l fails with bytes that are not UTF-8' do
162
+ runner.answers['crontab -l'] = ['', 1, "refus\xE9\e[2J".dup.force_encoding(Encoding::UTF_8)]
163
+
164
+ expect { adapter.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, 'cron could not read the crontab: refus??[2J')
165
+ end
166
+ end
167
+
168
+ describe RubyRaider::Schedules::Os::Linux do
169
+ let(:systemd) { RubyRaider::Schedules::Os::Systemd.new(kind: kind, runner: runner, home: home, config_home: nil) }
170
+ let(:cron) { RubyRaider::Schedules::Os::Cron.new(kind: kind, runner: runner) }
171
+ let(:units) { File.join(home, '.config', 'systemd', 'user') }
172
+
173
+ before do
174
+ touch(units, "#{prefix}timer_one.timer")
175
+ runner.answers['crontab -l'] = ["# raider-schedule #{prefix}cron_one\n", 0]
176
+ end
177
+
178
+ it 'with systemd answering: the unit files plus the cron tags' do
179
+ adapter = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
180
+
181
+ expect(adapter.list_ids(prefix)).to eq(["#{prefix}cron_one", "#{prefix}timer_one"])
182
+ end
183
+
184
+ it 'with no user bus: the cron tags plus the stranded unit files' do
185
+ runner.answers['systemctl --user show-environment'] = ['', 1, 'Failed to connect to bus']
186
+ adapter = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
187
+
188
+ expect(adapter.list_ids(prefix)).to eq(["#{prefix}cron_one", "#{prefix}timer_one"])
189
+ end
190
+
191
+ it 'leaves out the other scheduler when it fails, and raises when the primary one does' do
192
+ runner.answers['crontab -l'] = ['', 1, 'permission denied']
193
+ with_systemd = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
194
+ expect(with_systemd.list_ids(prefix)).to eq(["#{prefix}timer_one"])
195
+
196
+ runner.answers['systemctl --user show-environment'] = ['', 1, 'Failed to connect to bus']
197
+ with_cron = described_class.new(kind: kind, runner: runner, systemd: systemd, cron: cron)
198
+ expect { with_cron.list_ids(prefix) }.to raise_error(RubyRaider::Schedules::Error, /could not read the crontab/)
199
+ end
200
+ end
201
+ end
202
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'tmpdir'
4
- require_relative '../../../../lib/utilities/flows/os/systemd'
4
+ require_relative '../../../../lib/utilities/schedules/os/systemd'
5
+ require_relative '../../../../lib/utilities/schedules/schedule'
5
6
 
6
- # Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
7
7
  # The fake runner, temp home and project folders sit next to the adapter under test
8
8
  # rubocop:disable RSpec/MultipleMemoizedHelpers
9
- RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePath
9
+ RSpec.describe RubyRaider::Schedules::Os::Systemd do # rubocop:disable RSpec/FilePath
10
10
  # Records every command and answers from a table keyed by the systemctl verb; never runs anything
11
11
  let(:fake_runner_class) do
12
12
  Class.new do
@@ -21,7 +21,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
21
21
  @calls << argv
22
22
  @seen&.call(argv, stdin_data)
23
23
  stderr, status = answers.fetch(argv[2], ['', 0])
24
- RubyRaider::Flows::Os::CommandRunner::Result.new(stdout: '', stderr: stderr, exitstatus: status)
24
+ RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: '', stderr: stderr, exitstatus: status)
25
25
  end
26
26
 
27
27
  def on_call(&block) = @seen = block
@@ -29,18 +29,20 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
29
29
  end
30
30
 
31
31
  let(:hostile) { %q('; rm -rf ~; ' </string> %h $(id) " \\ &) }
32
- let(:tmp) { File.realpath(Dir.mktmpdir('flows-systemd')) }
32
+ let(:tmp) { File.realpath(Dir.mktmpdir('schedules-systemd')) }
33
33
  let(:home) { File.join(tmp, 'home') }
34
34
  let(:project_dir) { File.join(tmp, 'plain') }
35
35
  let(:runner) { fake_runner_class.new }
36
- let(:systemd) { described_class.new(runner: runner, home: home, config_home: nil) }
36
+ let(:systemd) { described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, config_home: nil) }
37
37
  let(:now) { Time.utc(2026, 11, 9, 10, 0, 0) }
38
38
  let(:units) { File.join(home, '.config', 'systemd', 'user') }
39
39
 
40
- def schedule(dir: project_dir, **options)
40
+ def schedule(dir: project_dir, gem_env: {}, bundle: '/opt/ruby/bin/bundle', **options)
41
41
  FileUtils.mkdir_p(dir)
42
- project = RubyRaider::Flows::Project.new(File.realpath(dir))
43
- RubyRaider::Flows::Schedule.build(project: project, name: 'report', every: 'day', at: '08:00', now: now, **options)
42
+ # Hermetic: the GEM_HOME/GEM_PATH and bundle this spec runs with never reach the entry; a spec passes its own
43
+ allow(RubyRaider::Schedules::Schedule).to receive_messages(captured_gem_env: gem_env, bundle_path: bundle)
44
+ project = RubyRaider::Schedules::Project.new(File.realpath(dir))
45
+ RubyRaider::Schedules::Schedule.build(project: project, name: 'report', every: 'day', at: '08:00', now: now, **options)
44
46
  end
45
47
 
46
48
  # systemd's specifier rule for a raw value: %% is %, any other % would be a specifier
@@ -110,7 +112,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
110
112
 
111
113
  expect(systemd.service(item)).to eq(<<~UNIT)
112
114
  [Unit]
113
- Description=raider flow report
115
+ Description=raider schedule report
114
116
 
115
117
  [Service]
116
118
  Type=oneshot
@@ -123,22 +125,24 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
123
125
  end
124
126
 
125
127
  it 'keeps GEM_HOME and GEM_PATH in the environment when they were captured' do
126
- allow(RubyRaider::Flows::Schedule).to receive(:captured_gem_env).and_return('GEM_HOME' => '/g/%h$x', 'GEM_PATH' => '/g/path')
128
+ item = schedule(gem_env: { 'GEM_HOME' => '/g/%h$x', 'GEM_PATH' => '/g/path' })
127
129
 
128
- lines = value_of(systemd.service(schedule), 'Environment').flat_map { |value| systemd_words(value, dollars: false) }
130
+ lines = value_of(systemd.service(item), 'Environment').flat_map { |value| systemd_words(value, dollars: false) }
129
131
 
130
132
  expect(lines.last(3)).to eq(['LANG=en_US.UTF-8', 'GEM_HOME=/g/%h$x', 'GEM_PATH=/g/path'])
131
133
  end
132
134
 
133
- describe 'with hostile values in an input and in the project path' do
134
- let(:item) { schedule(dir: File.join(tmp, "p#{hostile}<", "string> #{hostile}"), inputs: ["note=#{hostile}"]) }
135
+ describe 'with hostile values in the bundle path, GEM_HOME and the project path' do
136
+ let(:item) do
137
+ schedule(dir: File.join(tmp, "p#{hostile}<", "string> #{hostile}"), bundle: "/opt/#{hostile}/bundle", gem_env: { 'GEM_HOME' => "/g/#{hostile}" })
138
+ end
135
139
  let(:text) { systemd.service(item) }
136
140
 
137
141
  it 'gives back the exact argv from ExecStart' do
138
142
  argv = systemd_words(value_of(text, 'ExecStart').first, dollars: true)
139
143
 
140
144
  expect(argv).to eq(item.argv)
141
- expect(argv.last).to eq("note=#{hostile}")
145
+ expect(argv[1]).to eq("/opt/#{hostile}/bundle")
142
146
  expect(item.working_directory).to include('</string>', "'; rm -rf ~; '", '$(id)', '%h', '"', '\\', '&', ' ')
143
147
  end
144
148
 
@@ -153,7 +157,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
153
157
  it 'writes them only in escaped form' do
154
158
  expect(text).to include('%%h', '$$(id)', '\\" \\\\ &')
155
159
  expect(text.scan(/(?<!%)%(?!%)/)).to be_empty
156
- expect(text.lines.length).to eq(systemd.service(schedule).lines.length)
160
+ expect(text.lines.length).to eq(systemd.service(schedule(gem_env: { 'GEM_HOME' => '/g' })).lines.length)
157
161
  end
158
162
  end
159
163
  end
@@ -162,7 +166,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
162
166
  it 'writes the exact timer for a daily schedule' do
163
167
  expect(systemd.timer(schedule)).to eq(<<~UNIT)
164
168
  [Unit]
165
- Description=raider flow report schedule
169
+ Description=raider schedule report schedule
166
170
 
167
171
  [Timer]
168
172
  OnCalendar=*-*-* 08:00:00
@@ -196,8 +200,8 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
196
200
  it 'uses an absolute XDG_CONFIG_HOME and ignores a relative one' do
197
201
  item = schedule
198
202
  config = File.join(tmp, 'xdg')
199
- described_class.new(runner: runner, home: home, config_home: config).install(item)
200
- described_class.new(runner: runner, home: home, config_home: 'relative').install(item)
203
+ described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, config_home: config).install(item)
204
+ described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, home: home, config_home: 'relative').install(item)
201
205
 
202
206
  expect([File.exist?(File.join(config, 'systemd', 'user', "#{item.os_id}.timer")), File.exist?(timer_path(item))]).to eq([true, true])
203
207
  end
@@ -217,7 +221,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
217
221
  item = schedule
218
222
  runner.answers['enable'] = ["Failed to enable unit\e[2J\n", 1]
219
223
 
220
- expect { systemd.install(item) }.to raise_error(RubyRaider::Flows::Error, /systemd could not enable .*Failed to enable unit\?\[2J/)
224
+ expect { systemd.install(item) }.to raise_error(RubyRaider::Schedules::Error, /systemd could not enable .*Failed to enable unit\?\[2J/)
221
225
  expect(runner.calls.map { |argv| argv[2] }).to eq(%w[daemon-reload enable disable daemon-reload])
222
226
  expect(runner.calls[2]).to eq(['systemctl', '--user', 'disable', "#{item.os_id}.timer"])
223
227
  expect(Dir.children(units)).to be_empty
@@ -227,7 +231,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
227
231
  item = schedule
228
232
  runner.answers['daemon-reload'] = ['Failed to connect to bus', 1]
229
233
 
230
- expect { systemd.install(item) }.to raise_error(RubyRaider::Flows::Error, /Failed to connect to bus/)
234
+ expect { systemd.install(item) }.to raise_error(RubyRaider::Schedules::Error, /Failed to connect to bus/)
231
235
  expect(Dir.children(units)).to be_empty
232
236
  end
233
237
 
@@ -235,7 +239,7 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
235
239
  ['trailing ', 'tab\\'].each do |name|
236
240
  item = schedule(dir: File.join(tmp, name))
237
241
 
238
- expect { systemd.install(item) }.to raise_error(RubyRaider::Flows::Error, /systemd can not use the path/)
242
+ expect { systemd.install(item) }.to raise_error(RubyRaider::Schedules::Error, /systemd can not use the path/)
239
243
  end
240
244
  expect([runner.calls, File.exist?(units)]).to eq([[], false])
241
245
  end
@@ -246,15 +250,15 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
246
250
  original.call(path, *rest, **options) { raise Errno::ENOSPC }
247
251
  end
248
252
 
249
- expect { systemd.install(schedule) }.to raise_error(RubyRaider::Flows::Error, /Could not write .*No space left/)
253
+ expect { systemd.install(schedule) }.to raise_error(RubyRaider::Schedules::Error, /Could not write .*No space left/)
250
254
  expect([runner.calls, Dir.children(units)]).to eq([[], []])
251
255
  end
252
256
 
253
- it 'raises a flow error and runs nothing when it can not write the units' do
257
+ it 'raises a schedule error and runs nothing when it can not write the units' do
254
258
  FileUtils.mkdir_p(File.dirname(units))
255
259
  File.write(units, 'not a folder')
256
260
 
257
- expect { systemd.install(schedule) }.to raise_error(RubyRaider::Flows::Error, /Could not write .*systemd/)
261
+ expect { systemd.install(schedule) }.to raise_error(RubyRaider::Schedules::Error, /Could not write .*systemd/)
258
262
  expect([runner.calls, File.read(units)]).to eq([[], 'not a folder'])
259
263
  end
260
264
  end
@@ -282,17 +286,17 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
282
286
  item = schedule
283
287
  allow(File).to receive(:unlink).and_raise(Errno::EACCES)
284
288
 
285
- expect { systemd.remove(item.os_id) }.to raise_error(RubyRaider::Flows::Error, /Could not remove .*Permission denied/)
289
+ expect { systemd.remove(item.os_id) }.to raise_error(RubyRaider::Schedules::Error, /Could not remove .*Permission denied/)
286
290
  end
287
291
 
288
292
  it 'raises when the reload fails' do
289
293
  runner.answers['daemon-reload'] = ['Failed to connect to bus', 1]
290
294
 
291
- expect { systemd.remove(schedule.os_id) }.to raise_error(RubyRaider::Flows::Error, /Failed to connect to bus/)
295
+ expect { systemd.remove(schedule.os_id) }.to raise_error(RubyRaider::Schedules::Error, /Failed to connect to bus/)
292
296
  end
293
297
 
294
- it 'refuses an os_id that is not a flow schedule id' do
295
- expect { systemd.remove('../../evil') }.to raise_error(RubyRaider::Flows::Error, /Invalid os_id/)
298
+ it 'refuses an os_id that is not a schedule id' do
299
+ expect { systemd.remove('../../evil') }.to raise_error(RubyRaider::Schedules::Error, /Invalid os_id/)
296
300
  expect(runner.calls).to be_empty
297
301
  end
298
302
  end
@@ -320,8 +324,8 @@ RSpec.describe RubyRaider::Flows::Os::Systemd do # rubocop:disable RSpec/FilePat
320
324
  expect(systemd.present?(schedule.os_id)).to be(false)
321
325
  end
322
326
 
323
- it 'refuses an os_id that is not a flow schedule id and runs nothing' do
324
- expect { systemd.present?('x; rm') }.to raise_error(RubyRaider::Flows::Error, /Invalid os_id/)
327
+ it 'refuses an os_id that is not a schedule id and runs nothing' do
328
+ expect { systemd.present?('x; rm') }.to raise_error(RubyRaider::Schedules::Error, /Invalid os_id/)
325
329
  expect(runner.calls).to be_empty
326
330
  end
327
331
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require 'tmpdir'
4
4
  require 'rexml/document'
5
- require_relative '../../../../lib/utilities/flows/os/task_scheduler'
5
+ require_relative '../../../../lib/utilities/schedules/os/task_scheduler'
6
+ require_relative '../../../../lib/utilities/schedules/schedule'
6
7
 
7
- # Flow specs live under spec/utilities/flows, next to the code in lib/utilities/flows
8
8
  # The fake runner, fixed clock and project folders sit next to the adapter under test
9
9
  # rubocop:disable RSpec/MultipleMemoizedHelpers
10
- RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/FilePath
10
+ RSpec.describe RubyRaider::Schedules::Os::TaskScheduler do # rubocop:disable RSpec/FilePath
11
11
  # Records every command and answers from a table keyed by the schtasks verb; never runs anything
12
12
  let(:fake_runner_class) do
13
13
  Class.new do
@@ -22,7 +22,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
22
22
  @calls << argv
23
23
  @seen&.call(argv, stdin_data)
24
24
  stderr, status, stdout = answers.fetch(argv[1], ['', 0])
25
- RubyRaider::Flows::Os::CommandRunner::Result.new(stdout: stdout.to_s, stderr: stderr, exitstatus: status)
25
+ RubyRaider::Schedules::Os::CommandRunner::Result.new(stdout: stdout.to_s, stderr: stderr, exitstatus: status)
26
26
  end
27
27
 
28
28
  def on_call(&block) = @seen = block
@@ -30,18 +30,20 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
30
30
  end
31
31
 
32
32
  let(:hostile) { %q('; rm -rf ~; ' </string> %h $(id) " \\ &) }
33
- let(:tmp) { File.realpath(Dir.mktmpdir('flows-schtasks')) }
33
+ let(:tmp) { File.realpath(Dir.mktmpdir('schedules-schtasks')) }
34
34
  let(:project_dir) { File.join(tmp, 'plain') }
35
35
  let(:runner) { fake_runner_class.new }
36
36
  let(:clock) { Struct.new(:now).new(Time.new(2026, 11, 9, 10, 30, 0)) }
37
37
  let(:schtasks) { 'C:\\Windows\\System32\\schtasks.exe' }
38
- let(:scheduler) { described_class.new(runner: runner, clock: clock, schtasks: schtasks) }
38
+ let(:scheduler) { described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, clock: clock, schtasks: schtasks) }
39
39
  let(:now) { Time.utc(2026, 11, 9, 10, 0, 0) }
40
40
 
41
- def schedule(dir: project_dir, **options)
41
+ def schedule(dir: project_dir, gem_env: {}, bundle: '/opt/ruby/bin/bundle', **options)
42
42
  FileUtils.mkdir_p(dir)
43
- project = RubyRaider::Flows::Project.new(File.realpath(dir))
44
- RubyRaider::Flows::Schedule.build(project: project, name: 'report', every: 'day', at: '08:00', now: now, **options)
43
+ # Hermetic: the GEM_HOME/GEM_PATH and bundle this spec runs with never reach the entry; a spec passes its own
44
+ allow(RubyRaider::Schedules::Schedule).to receive_messages(captured_gem_env: gem_env, bundle_path: bundle)
45
+ project = RubyRaider::Schedules::Project.new(File.realpath(dir))
46
+ RubyRaider::Schedules::Schedule.build(project: project, name: 'report', every: 'day', at: '08:00', now: now, **options)
45
47
  end
46
48
 
47
49
  def task_name(item) = "\\RubyRaider\\#{item.os_id}"
@@ -193,7 +195,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
193
195
  <?xml version="1.0" encoding="UTF-16"?>
194
196
  <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
195
197
  <RegistrationInfo>
196
- <Description>Ruby Raider flow report (every day at 08:00)</Description>
198
+ <Description>Ruby Raider schedule report (every day at 08:00)</Description>
197
199
  <URI>\\RubyRaider\\#{item.os_id}</URI>
198
200
  </RegistrationInfo>
199
201
  <Triggers>
@@ -279,7 +281,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
279
281
  end
280
282
 
281
283
  it "refuses an argument with both ' and a glob character" do
282
- expect { described_class.quote("a'*") }.to raise_error(RubyRaider::Flows::Error, /can not pass "a'\*" to ruby\.exe/)
284
+ expect { described_class.quote("a'*") }.to raise_error(RubyRaider::Schedules::Error, /can not pass "a'\*" to ruby\.exe/)
283
285
  end
284
286
  end
285
287
 
@@ -290,13 +292,14 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
290
292
  end
291
293
 
292
294
  it 'carries no environment and no log file' do
293
- text = scheduler.task_xml(schedule)
295
+ # Without the random temp folder's name, which may hold any of these words
296
+ text = scheduler.task_xml(schedule).gsub(tmp, '')
294
297
 
295
298
  expect(text).not_to include('scheduler.log', 'Environment', 'GEM_HOME', 'cmd', 'powershell')
296
299
  end
297
300
 
298
- it 'runs ruby with the bundle path and the flow run arguments' do
299
- item = schedule(retries: 2, inputs: %w[env=staging])
301
+ it 'runs ruby with the bundle path and the schedule run arguments' do
302
+ item = schedule(retries: 2, selection: RubyRaider::Schedules::Selection.new(kind: 'tag', tag: 'smoke'))
300
303
  text = scheduler.task_xml(item)
301
304
 
302
305
  expect(field(text, 'Task/Actions/Exec/Command')).to eq(item.ruby)
@@ -338,7 +341,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
338
341
 
339
342
  # One at a time: a lone % in both would read as %...% across the joined Arguments and be refused
340
343
  {
341
- 'an input value' => ->(spec) { spec.schedule(inputs: ["note=#{spec.hostile}"]) },
344
+ 'the bundle path' => ->(spec) { spec.schedule(bundle: "/opt/#{spec.hostile}/bundle") },
342
345
  'the project path' => ->(spec) { spec.schedule(dir: File.join(spec.tmp, 'p<', "string> #{spec.hostile}")) }
343
346
  }.each do |where, build|
344
347
  describe "with hostile values in #{where}" do
@@ -363,46 +366,49 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
363
366
  end
364
367
 
365
368
  describe 'with quotes and glob characters but no spaces' do
366
- let(:item) { schedule(dir: File.join(tmp, 'Users', "O'Brien", 'proj'), inputs: ["a=x'", "b=y'", 'c=p[1]', 'd=a*b']) }
369
+ let(:item) { schedule(dir: File.join(tmp, 'Users', "O'Brien", "proj'"), bundle: '/opt/p[1]/a*b/bundle') }
367
370
 
368
371
  it "quotes them so ruby.exe reads the exact argv back and globs nothing ('...' around a glob)" do
369
372
  line = field(scheduler.task_xml(item), 'Task/Actions/Exec/Arguments')
370
373
 
371
374
  expect(ruby_split_arguments(line)).to eq(item.argv.drop(1))
372
- expect(line).to include(%("#{item.project_root}"), %("a=x'"), %("b=y'"), "'c=p[1]'", "'d=a*b'")
375
+ expect(line).to include(%("#{item.project_root}"), "'/opt/p[1]/a*b/bundle'")
373
376
  end
374
377
 
375
- it "refuses an input with both ' and a glob character before any schtasks call" do
376
- expect { scheduler.install(schedule(inputs: ["e=a'*"])) }.to raise_error(RubyRaider::Flows::Error, /ruby\.exe/)
378
+ it "refuses an argument with both ' and a glob character before any schtasks call" do
379
+ expect { scheduler.install(schedule(bundle: "/opt/a'*/bundle")) }.to raise_error(RubyRaider::Schedules::Error, /ruby\.exe/)
377
380
  expect(runner.calls).to be_empty
378
381
  end
379
382
  end
380
383
 
381
384
  describe 'with %NAME% that Task Scheduler would expand' do
382
- it 'refuses it in an input value before any schtasks call' do
383
- item = schedule(inputs: ['note=%PATH%'])
385
+ it 'refuses it in the bundle path before any schtasks call' do
386
+ item = schedule(bundle: '/opt/%PATH%/bundle')
384
387
 
385
- expect { scheduler.install(item) }.to raise_error(RubyRaider::Flows::Error, /Task Scheduler would expand %PATH% as an environment variable/)
388
+ expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, /Task Scheduler would expand %PATH% as an environment variable/)
386
389
  expect(runner.calls).to be_empty
387
390
  end
388
391
 
389
392
  it 'refuses it in the project path before any schtasks call' do
390
393
  item = schedule(dir: File.join(tmp, '%PATH%', 'proj'))
391
394
 
392
- expect { scheduler.install(item) }.to raise_error(RubyRaider::Flows::Error, /would expand %PATH%/)
393
- expect { scheduler.task_xml(item) }.to raise_error(RubyRaider::Flows::Error, /would expand %PATH%/)
395
+ expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, /would expand %PATH%/)
396
+ expect { scheduler.task_xml(item) }.to raise_error(RubyRaider::Schedules::Error, /would expand %PATH%/)
394
397
  expect(runner.calls).to be_empty
395
398
  end
396
399
 
397
400
  it 'refuses a %...% formed across two arguments in the joined Arguments' do
398
- expect { scheduler.install(schedule(inputs: ['a=%x', 'b=y%'])) }.to raise_error(RubyRaider::Flows::Error, /would expand %x b=y%/)
401
+ item = schedule(dir: File.join(tmp, 'y%'), bundle: '/opt/%x/bundle')
402
+
403
+ expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, %r{would expand %x/bundle exec raider schedule run report --project .*/y%})
399
404
  expect(runner.calls).to be_empty
400
405
  end
401
406
 
402
407
  it 'keeps a lone %' do
403
- item = schedule(inputs: ['note=100% sure'])
408
+ item = schedule(dir: File.join(tmp, '100% sure'))
404
409
 
405
- expect(split_arguments(field(scheduler.task_xml(item), 'Task/Actions/Exec/Arguments')).last).to eq('note=100% sure')
410
+ expect(split_arguments(field(scheduler.task_xml(item), 'Task/Actions/Exec/Arguments'))).to eq(item.argv.drop(1))
411
+ expect(item.project_root).to end_with('/100% sure')
406
412
  end
407
413
  end
408
414
 
@@ -426,7 +432,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
426
432
  runner.on_call { |argv, _| path = argv[5] }
427
433
  runner.answers['/Create'] = ["ERROR: Access is denied.\e[2J\n", 1]
428
434
 
429
- expect { scheduler.install(item) }.to raise_error(RubyRaider::Flows::Error, /Task Scheduler.*#{Regexp.escape(item.os_id)}.*Access is denied\.\?\[2J/)
435
+ expect { scheduler.install(item) }.to raise_error(RubyRaider::Schedules::Error, /Task Scheduler.*#{Regexp.escape(item.os_id)}.*Access is denied\.\?\[2J/)
430
436
  expect(File.exist?(path)).to be(false)
431
437
  end
432
438
  end
@@ -453,18 +459,18 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
453
459
  runner.answers['/Delete'] = ["ERROR: Access\e is denied.\n", 1]
454
460
  runner.answers['/Query'] = ['', 0, csv_rows('\\Other', task_name(item))]
455
461
 
456
- expect { scheduler.remove(item.os_id) }.to raise_error(RubyRaider::Flows::Error, /Task Scheduler could not delete .*Access\? is denied\.\z/)
462
+ expect { scheduler.remove(item.os_id) }.to raise_error(RubyRaider::Schedules::Error, /Task Scheduler could not delete .*Access\? is denied\.\z/)
457
463
  end
458
464
 
459
465
  it 'raises with the delete error when the task list can not be read' do
460
466
  runner.answers['/Delete'] = ['ERROR: Access is denied.', 1]
461
467
  runner.answers['/Query'] = ['ERROR: list failed', 1]
462
468
 
463
- expect { scheduler.remove(schedule.os_id) }.to raise_error(RubyRaider::Flows::Error, /could not delete .*Access is denied/)
469
+ expect { scheduler.remove(schedule.os_id) }.to raise_error(RubyRaider::Schedules::Error, /could not delete .*Access is denied/)
464
470
  end
465
471
 
466
- it 'refuses an os_id that is not a flow schedule id' do
467
- expect { scheduler.remove('..\\..\\evil') }.to raise_error(RubyRaider::Flows::Error, /Invalid os_id/)
472
+ it 'refuses an os_id that is not a schedule id' do
473
+ expect { scheduler.remove('..\\..\\evil') }.to raise_error(RubyRaider::Schedules::Error, /Invalid os_id/)
468
474
  expect(runner.calls).to be_empty
469
475
  end
470
476
  end
@@ -488,7 +494,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
488
494
  it 'raises when the task list can not be read' do
489
495
  runner.answers['/Query'] = ["ERROR: no\e list", 1]
490
496
 
491
- expect { scheduler.present?(schedule.os_id) }.to raise_error(RubyRaider::Flows::Error, /could not list tasks: ERROR: no\? list/)
497
+ expect { scheduler.present?(schedule.os_id) }.to raise_error(RubyRaider::Schedules::Error, /could not list tasks: ERROR: no\? list/)
492
498
  end
493
499
  end
494
500
 
@@ -497,7 +503,7 @@ RSpec.describe RubyRaider::Flows::Os::TaskScheduler do # rubocop:disable RSpec/F
497
503
  allow(ENV).to receive(:fetch).with('SystemRoot', anything).and_return('D:\\Win')
498
504
  item = schedule
499
505
 
500
- described_class.new(runner: runner, clock: clock).remove(item.os_id)
506
+ described_class.new(kind: RubyRaider::Schedules::Kind::TESTS, runner: runner, clock: clock).remove(item.os_id)
501
507
 
502
508
  expect(runner.calls.first.first).to eq(File.join('D:\\Win', 'System32', 'schtasks.exe'))
503
509
  end