pwn 0.5.782 → 0.5.783
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 +4 -4
- data/documentation/Installation.md +33 -9
- data/documentation/pwn-REPL.md +31 -3
- data/documentation/pwn-ai-Agent.md +62 -2
- data/etc/default_cron/session_cleanup.rb +335 -42
- data/lib/pwn/ai/agent/curriculum.rb +2 -2
- data/lib/pwn/ai/agent/learning.rb +11 -4
- data/lib/pwn/ai/agent/mistakes.rb +22 -3
- data/lib/pwn/ai/agent/tools/sessions.rb +6 -6
- data/lib/pwn/ai/http_retry.rb +3 -0
- data/lib/pwn/ai/router.rb +3 -1
- data/lib/pwn/cron.rb +1 -1
- data/lib/pwn/memory.rb +62 -6
- data/lib/pwn/plugins/repl/ai.rb +13 -3
- data/lib/pwn/plugins/repl/ai_console.rb +37 -21
- data/lib/pwn/session_trace.rb +1 -1
- data/lib/pwn/sessions.rb +305 -80
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/learning_spec.rb +65 -2
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +3 -3
- data/spec/lib/pwn/memory_spec.rb +58 -1
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +191 -6
- data/spec/lib/pwn/plugins/repl/ai_result_chronology_spec.rb +2 -2
- data/spec/lib/pwn/plugins/repl/ai_spec.rb +28 -11
- data/spec/lib/pwn/session_cleanup_default_spec.rb +2 -2
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +467 -36
- data/spec/lib/pwn/sessions_spec.rb +116 -16
- data/third_party/pwn_rdoc.jsonl +12 -0
- metadata +1 -1
|
@@ -14,6 +14,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
14
14
|
before do
|
|
15
15
|
allow(Dir).to receive(:home).and_return(File.join(@tmp, 'home'))
|
|
16
16
|
FileUtils.mkdir_p(File.join(Dir.home, '.pwn'))
|
|
17
|
+
stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(Dir.home, '.pwn', 'learning.jsonl'))
|
|
17
18
|
PWN::Sessions.create(id: id)
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -21,6 +22,340 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
21
22
|
File.write(meta_path, JSON.generate({ id: id, title: title, source: 'pwn-ai' }.merge(value)))
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
describe 'reader-based historical retention' do
|
|
26
|
+
it 'keeps incomplete provenance records and reusable swarm associations, including retired registry names' do
|
|
27
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
28
|
+
root = File.join(Dir.home, '.pwn')
|
|
29
|
+
swarm = PWN::AI::Agent::Swarm
|
|
30
|
+
stub_const('PWN::AI::Agent::Swarm::SWARM_ROOT', File.join(root, 'swarm'))
|
|
31
|
+
swarm_id = swarm.create[:swarm_id]
|
|
32
|
+
swarm.spawn(name: 'fixture', role: 'fixture', swarm_id: swarm_id)
|
|
33
|
+
mapping = File.join(root, 'swarm', swarm_id, 'personas.json')
|
|
34
|
+
File.write(mapping, JSON.generate(fixture: id))
|
|
35
|
+
swarm.retire(name: 'fixture', swarm_id: swarm_id, local: true)
|
|
36
|
+
# Retire edits only agents.yml: persona_session still intentionally reuses
|
|
37
|
+
# this mapping for an explicit persona or a later respawn. Age is not exit.
|
|
38
|
+
stores = {
|
|
39
|
+
'mistakes.json' => { incomplete: { sessions: [id] } },
|
|
40
|
+
'memory.json' => { "reflect_#{id}_abcdef12" => { category: 'lesson' } },
|
|
41
|
+
'extrospection.json' => { observations: [{ source: 'auto_extrospect', tags: ['drift', id] }] },
|
|
42
|
+
'curriculum/log.jsonl' => { event: 'practice', data: [{ runs: [{ session_id: id }] }] }
|
|
43
|
+
}
|
|
44
|
+
stores.each do |name, row|
|
|
45
|
+
FileUtils.mkdir_p(File.dirname(File.join(root, name)))
|
|
46
|
+
File.write(File.join(root, name), JSON.generate(row))
|
|
47
|
+
end
|
|
48
|
+
expected = stores.keys.map { |name| File.join(root, name) } + [mapping]
|
|
49
|
+
expect(PWN::Sessions.dependency_index[:dependencies][id]).to match_array(expected)
|
|
50
|
+
expect(described_class.run(dry_run: false)[:deleted_ids]).to be_empty
|
|
51
|
+
expect(File.exist?(path)).to be(true)
|
|
52
|
+
expect(swarm.send(:persona_session, swarm_id: swarm_id, name: 'fixture')).to eq(id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'preserves reflection values, drift observations and practice results without retaining their provenance transcripts' do
|
|
56
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
57
|
+
root = File.join(Dir.home, '.pwn')
|
|
58
|
+
stub_const('PWN::Memory::MEMORY_FILE', File.join(root, 'memory.json'))
|
|
59
|
+
stub_const('PWN::AI::Agent::Extrospection::EXTRO_FILE', File.join(root, 'extrospection.json'))
|
|
60
|
+
stub_const('PWN::AI::Agent::Curriculum::CURRICULUM_DIR', File.join(root, 'curriculum'))
|
|
61
|
+
PWN::Memory.remember(key: "reflect_#{id}_abcdef12", value: 'Use verified results café', category: :lesson, source: :reflect)
|
|
62
|
+
PWN::AI::Agent::Extrospection.observe(source: 'auto_extrospect', category: :env, data: 'Host changed café', tags: ['drift', id])
|
|
63
|
+
run = { session_id: id, prompt: 'fixture', final: nil, trace: nil, score: nil, verdict: 'unknown', training_score: nil }
|
|
64
|
+
PWN::AI::Agent::Curriculum.send(:log, event: :practice, data: [{ signature: 'fixture', tool: 'shell', prompts: ['fixture'], resolved: false, runs: [run] }])
|
|
65
|
+
stores = %w[memory.json extrospection.json curriculum/log.jsonl].to_h { |name| [File.join(root, name), File.binread(File.join(root, name))] }
|
|
66
|
+
expect(PWN::Sessions.dependency_index[:dependencies].fetch(id, [])).to be_empty
|
|
67
|
+
expect(described_class.run(dry_run: false)[:deleted_ids]).to include(id)
|
|
68
|
+
stores.each { |file, bytes| expect(File.binread(file)).to eq(bytes) }
|
|
69
|
+
expect(PWN::Memory.recall(query: "reflect_#{id}_abcdef12", include_session: false).values.first[:value]).to eq('Use verified results café')
|
|
70
|
+
expect(PWN::AI::Agent::Extrospection.observations.first[:data]).to eq('Host changed café')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'removes transcripts without altering self-contained mistake history or its readers' do
|
|
74
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
75
|
+
store = File.join(Dir.home, '.pwn', 'mistakes.json')
|
|
76
|
+
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', store)
|
|
77
|
+
mistake = PWN::AI::Agent::Mistakes.record(tool: 'fixture', error: 'Unicode failure café', session_id: id)
|
|
78
|
+
PWN::AI::Agent::Mistakes.record(tool: 'unrelated', error: 'unrelated retained failure', session_id: '20200101_010203_abcdef99')
|
|
79
|
+
bytes = File.binread(store)
|
|
80
|
+
expect(PWN::Sessions.dependency_index[:dependencies].fetch(id, [])).not_to include(store)
|
|
81
|
+
expect(PWN::Sessions.protected_session_ids(hot_days: 0)).not_to include(id)
|
|
82
|
+
expect(described_class.run(dry_run: false, current_session_id: id)[:deleted_ids]).to be_empty
|
|
83
|
+
lease = PWN::Sessions.activate(session_id: id)
|
|
84
|
+
begin
|
|
85
|
+
expect(described_class.run(dry_run: false)[:deleted_ids]).to be_empty
|
|
86
|
+
expect(File.exist?(path)).to be(true)
|
|
87
|
+
ensure
|
|
88
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
89
|
+
end
|
|
90
|
+
result = described_class.run(dry_run: false)
|
|
91
|
+
expect(result[:deleted_ids]).to include(id)
|
|
92
|
+
expect(File.exist?(path)).to be(false)
|
|
93
|
+
expect(File.binread(store)).to eq(bytes)
|
|
94
|
+
expect(PWN::AI::Agent::Mistakes.for_tool(tool: 'fixture').first).to eq(mistake)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe 'offline storage layout' do
|
|
99
|
+
it 'preserves externally anchored metadata and legacy trace paths rather than breaking evidence' do
|
|
100
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
101
|
+
PWN::SessionTrace.append(session_id: id, event: 'request', data: 'fixture')
|
|
102
|
+
trace = File.join(PWN::Sessions.sessions_dir, id, 'trace.jsonl')
|
|
103
|
+
File.write(File.join(Dir.home, '.pwn', 'findings.jsonl'), JSON.generate(session_id: id, evidence_paths: [meta_path, trace]))
|
|
104
|
+
result = described_class.reorganize(dry_run: false, writers_stopped: true)
|
|
105
|
+
expect(File.file?(meta_path)).to be(true)
|
|
106
|
+
expect(File.file?(trace)).to be(true)
|
|
107
|
+
expect(result[:retained_support]).to include(meta_path, trace)
|
|
108
|
+
expect(PWN::SessionTrace.read(session_id: id).length).to eq(1)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'requires stopped writers and preserves transcripts, metadata and lock inode through migration' do
|
|
112
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
113
|
+
lock_path = PWN::Sessions.support_path(kind: :lease, session_id: id)
|
|
114
|
+
inode = File.stat(lock_path).ino
|
|
115
|
+
bytes = File.binread(path)
|
|
116
|
+
lease = PWN::Sessions.activate(session_id: id)
|
|
117
|
+
expect { described_class.reorganize(dry_run: false, writers_stopped: true) }.to raise_error(IOError, /active/)
|
|
118
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
119
|
+
expect { described_class.reorganize(dry_run: false) }.to raise_error(ArgumentError, /stopped/)
|
|
120
|
+
result = described_class.reorganize(dry_run: false, writers_stopped: true)
|
|
121
|
+
expect(result[:state]).to eq('committed')
|
|
122
|
+
expect(File.binread(path)).to eq(bytes)
|
|
123
|
+
expect(File.stat(PWN::Sessions.support_path(kind: :lease, session_id: id)).ino).to eq(inode)
|
|
124
|
+
expect(File.exist?(meta_path)).to be(false)
|
|
125
|
+
expect(PWN::Sessions.metadata(session_id: id)[:title]).to eq(title)
|
|
126
|
+
expect(PWN::Sessions.dependency_index[:errors]).to be_empty
|
|
127
|
+
lease = PWN::Sessions.activate(session_id: id)
|
|
128
|
+
expect { described_class.reorganize(dry_run: false, writers_stopped: true) }.to raise_error(IOError, /active/)
|
|
129
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
130
|
+
expect(described_class.run(dry_run: false)[:deleted_ids]).to eq([id])
|
|
131
|
+
ensure
|
|
132
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe 'maintenance writer ownership' do
|
|
137
|
+
it 'admits only one maintenance writer across default and approved entry points' do
|
|
138
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
139
|
+
entered = Queue.new
|
|
140
|
+
release = Queue.new
|
|
141
|
+
allow(PWN::AI::Agent::Learning).to receive(:migrate_evidence).and_wrap_original do |method, **options|
|
|
142
|
+
unless options[:dry_run]
|
|
143
|
+
entered << true
|
|
144
|
+
release.pop
|
|
145
|
+
end
|
|
146
|
+
method.call(**options)
|
|
147
|
+
end
|
|
148
|
+
worker = Thread.new { described_class.maintain }
|
|
149
|
+
Timeout.timeout(2) { entered.pop }
|
|
150
|
+
expect { described_class.run(dry_run: false, approved_candidates: {}) }.to raise_error(IOError, /maintenance already running/)
|
|
151
|
+
read, write = IO.pipe
|
|
152
|
+
child = fork do
|
|
153
|
+
read.close
|
|
154
|
+
begin
|
|
155
|
+
described_class.run(dry_run: false, approved_candidates: {})
|
|
156
|
+
write.write('unexpected writer')
|
|
157
|
+
rescue IOError => e
|
|
158
|
+
write.write(e.message)
|
|
159
|
+
ensure
|
|
160
|
+
write.close
|
|
161
|
+
exit! 0
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
write.close
|
|
165
|
+
expect(Timeout.timeout(2) { read.read }).to include('maintenance already running')
|
|
166
|
+
Process.wait(child)
|
|
167
|
+
read.close
|
|
168
|
+
expect(described_class.maintain(dry_run: true)[:dry_run]).to be(true)
|
|
169
|
+
release << true
|
|
170
|
+
expect(worker.join(5)).to eq(worker)
|
|
171
|
+
expect(worker.value[:deleted_ids]).to eq([id])
|
|
172
|
+
ensure
|
|
173
|
+
release << true if release
|
|
174
|
+
worker&.join(5)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'does not wait on a title provider and retains its active session lease' do
|
|
178
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
179
|
+
PWN::Sessions.append(session_id: id, role: 'user', content: 'Fixture title topic')
|
|
180
|
+
other = PWN::Sessions.create[:id]
|
|
181
|
+
entered = Queue.new
|
|
182
|
+
release = Queue.new
|
|
183
|
+
allow(PWN::AI::Router).to receive(:summarize) do
|
|
184
|
+
entered << true
|
|
185
|
+
release.pop
|
|
186
|
+
JSON.generate(title: 'Fixture topic summary', summary: 'Offline fixture summary')
|
|
187
|
+
end
|
|
188
|
+
worker = Thread.new { PWN::Sessions.refresh_title(session_id: id) }
|
|
189
|
+
Timeout.timeout(2) { entered.pop }
|
|
190
|
+
result = Timeout.timeout(3) { described_class.maintain }
|
|
191
|
+
expect(result[:deleted_ids]).to eq([other])
|
|
192
|
+
expect(result[:records].find { |row| row[:id] == id }[:skip]).to eq(:active)
|
|
193
|
+
release << true
|
|
194
|
+
expect(worker.join(2)).to eq(worker)
|
|
195
|
+
expect(PWN::Sessions.title_status(session_id: id)[:is_summarized]).to be(true)
|
|
196
|
+
ensure
|
|
197
|
+
release << true if release
|
|
198
|
+
worker&.join(2)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
RSpec.describe PWN::SessionCleanup do
|
|
204
|
+
include_context 'pwn tmp sandbox'
|
|
205
|
+
|
|
206
|
+
let(:id) { '20200101_010203_abcdef12' }
|
|
207
|
+
let(:title) { "pwn-ai session #{id}" }
|
|
208
|
+
let(:path) { File.join(PWN::Sessions.sessions_dir, "#{id}.jsonl") }
|
|
209
|
+
let(:meta_path) { path.sub('.jsonl', '.meta.json') }
|
|
210
|
+
|
|
211
|
+
before do
|
|
212
|
+
allow(Dir).to receive(:home).and_return(File.join(@tmp, 'home'))
|
|
213
|
+
FileUtils.mkdir_p(File.join(Dir.home, '.pwn'))
|
|
214
|
+
stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(Dir.home, '.pwn', 'learning.jsonl'))
|
|
215
|
+
PWN::Sessions.create(id: id)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def metadata(value)
|
|
219
|
+
File.write(meta_path, JSON.generate({ id: id, title: title, source: 'pwn-ai' }.merge(value)))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'runs evidence preservation and deletion through the default maintenance path idempotently' do
|
|
223
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
224
|
+
PWN::Sessions.append(session_id: id, role: 'user', content: 'offline evidence fixture café')
|
|
225
|
+
PWN::Sessions.append(session_id: id, role: 'tool', content: 'verified tool observation 日本語')
|
|
226
|
+
PWN::Sessions.append(session_id: id, role: 'assistant', content: 'fixture final')
|
|
227
|
+
mistakes = File.join(Dir.home, '.pwn', 'mistakes.json')
|
|
228
|
+
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', mistakes)
|
|
229
|
+
PWN::AI::Agent::Mistakes.record(tool: 'fixture', error: 'fixture failure', session_id: id)
|
|
230
|
+
mistake_bytes = File.binread(mistakes)
|
|
231
|
+
File.write(PWN::AI::Agent::Learning::LEARNING_FILE, "#{JSON.generate(session_id: id, outcome: 'fixture')}\n")
|
|
232
|
+
original = PWN::Sessions.load(session_id: id)
|
|
233
|
+
plan = described_class.maintain(dry_run: true)
|
|
234
|
+
expect(plan[:evidence_migration][:migratable_sessions]).to eq(1)
|
|
235
|
+
expect(File).to exist(path)
|
|
236
|
+
result = described_class.maintain
|
|
237
|
+
expect(result[:deleted_ids]).to eq([id])
|
|
238
|
+
expect(PWN::Sessions.load(session_id: id)).to eq(original)
|
|
239
|
+
expect(File.binread(mistakes)).to eq(mistake_bytes)
|
|
240
|
+
row = JSON.parse(File.read(PWN::AI::Agent::Learning::LEARNING_FILE), symbolize_names: true)
|
|
241
|
+
expect(PWN::AI::Agent::Learning.evidence_complete?(row: row)).to be(true)
|
|
242
|
+
evidence = File.join(Dir.home, '.pwn', 'learning', 'evidence', "#{row[:evidence_ref][:sha256]}.json")
|
|
243
|
+
expect(Digest::SHA256.file(evidence).hexdigest).to eq(row[:evidence_ref][:sha256])
|
|
244
|
+
PWN::AI::Agent::Reward.send(:annotate_session, session_id: id, rewards: [1])
|
|
245
|
+
expect(PWN::Sessions.load(session_id: id).find { |entry| entry[:role] == 'tool' }[:step_reward]).to eq(1)
|
|
246
|
+
expect(Digest::SHA256.file(evidence).hexdigest).to eq(row[:evidence_ref][:sha256])
|
|
247
|
+
expect(PWN::Sessions.catalog[:sessions]).to eq([])
|
|
248
|
+
expect(described_class.maintain[:deleted_ids]).to eq([])
|
|
249
|
+
expect(result[:maintenance_ledger]).to be_a(String)
|
|
250
|
+
expect(JSON.parse(File.read(result[:maintenance_ledger]))['state']).to eq('committed')
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'validates versioned request checkpoints as provenance rather than transcript dependencies' do
|
|
254
|
+
runtime = PWN::AI::Agent::RequestRuntime.new(request: 'offline fixture', session_id: id, request_id: 'fixture-request')
|
|
255
|
+
index = PWN::Sessions.dependency_index
|
|
256
|
+
expect(index[:errors]).to eq([])
|
|
257
|
+
expect(index[:dependencies].fetch(id, [])).not_to include(runtime.checkpoint_path)
|
|
258
|
+
expect(index[:provenance][id]).to eq([runtime.checkpoint_path])
|
|
259
|
+
data = JSON.parse(File.read(runtime.checkpoint_path))
|
|
260
|
+
data['version'] = 99
|
|
261
|
+
File.write(runtime.checkpoint_path, JSON.generate(data))
|
|
262
|
+
index = PWN::Sessions.dependency_index
|
|
263
|
+
expect(index[:errors]).to include(runtime.checkpoint_path)
|
|
264
|
+
expect(index[:diagnostics].map { |row| row[:code] }).to include('Unsupported request checkpoint version')
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it 'paginates visible retained sessions and reports global diagnostics only once' do
|
|
268
|
+
ids = [id, '20200101_010203_abcdef13', '20200101_010203_abcdef14']
|
|
269
|
+
ids.drop(1).each { |sid| PWN::Sessions.create(id: sid) }
|
|
270
|
+
File.write(File.join(Dir.home, '.pwn', 'learning.jsonl'), ids.map { |sid| JSON.generate(session_id: sid) }.join("\n"))
|
|
271
|
+
File.write(File.join(Dir.home, '.pwn', 'memory.json'), '{')
|
|
272
|
+
page = PWN::Sessions.catalog(offset: 0, limit: 2)
|
|
273
|
+
expect(page).to include(total: 3, has_more: true)
|
|
274
|
+
expect(page[:sessions].length).to eq(2)
|
|
275
|
+
expect(page[:sessions].flat_map(&:keys)).not_to include(:unresolved_dependency_stores, :dependency_errors)
|
|
276
|
+
expect(page[:unresolved_dependency_stores].length).to eq(1)
|
|
277
|
+
last = PWN::Sessions.catalog(offset: 2, limit: 2)
|
|
278
|
+
expect(last).to include(total: 3, has_more: false)
|
|
279
|
+
expect((page[:sessions] + last[:sessions]).map { |row| row[:id] }).to match_array(ids)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'shares default command, seeded cron and legacy cron maintenance behavior' do
|
|
283
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
284
|
+
PWN::Sessions.append(session_id: id, role: 'user', content: 'retained café 日本語 🔒 fixture')
|
|
285
|
+
File.write(PWN::AI::Agent::Learning::LEARNING_FILE, JSON.generate(session_id: id, task: 'résumé'))
|
|
286
|
+
expect { PWN::Plugins::REPL.pwn_ai_run_sessions(args: ['cleanup']) }.to output(/deleted: 1/).to_stdout
|
|
287
|
+
expect(File).not_to exist(path)
|
|
288
|
+
expect(PWN::Sessions.load(session_id: id)).not_to be_empty
|
|
289
|
+
cron_id = PWN::Sessions.create[:id]
|
|
290
|
+
payload = PWN::Cron::DEFAULT_JOBS.find { |job| job[:name] == 'session_cleanup' }[:ruby]
|
|
291
|
+
allow(PWN::Cron).to receive(:cron_dir).and_return(File.expand_path('../../../etc/default_cron', __dir__))
|
|
292
|
+
expect(eval(payload)[:deleted_ids]).to eq([cron_id]) # rubocop:disable Security/Eval -- execute the exact bundled local job
|
|
293
|
+
legacy_id = PWN::Sessions.create[:id]
|
|
294
|
+
expect(described_class.run(dry_run: false)[:deleted_ids]).to eq([legacy_id])
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
it 'does not migrate evidence or delete while legacy ownership is unresolved' do
|
|
298
|
+
allow(described_class).to receive(:legacy_owners).and_return([{ pid: 123, reason: 'legacy_session_ownership_unknown' }])
|
|
299
|
+
expect(PWN::AI::Agent::Learning).to receive(:migrate_evidence).with(dry_run: true).and_call_original
|
|
300
|
+
expect(described_class.maintain[:apply_blocked]).to be(true)
|
|
301
|
+
expect(File).to exist(path)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
%w[memory.json memory.archive.json mistakes.json].each do |store|
|
|
305
|
+
it "remediates only the attributable invalid map entry in #{store}" do
|
|
306
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
307
|
+
target = File.join(Dir.home, '.pwn', store)
|
|
308
|
+
key = store == 'mistakes.json' ? 'bad' : "reflect_#{id}_abcdef12"
|
|
309
|
+
invalid = store == 'mistakes.json' ? { sessions: [id, 123] } : 123
|
|
310
|
+
valid = store == 'mistakes.json' ? { sessions: [], description: 'résumé 日本語 🔒' } : { value: 'keep résumé 日本語 🔒', category: 'fact' }
|
|
311
|
+
File.write(target, JSON.generate(key => invalid, 'unrelated' => valid))
|
|
312
|
+
expect(PWN::Sessions.dependency_index[:session_errors].keys).to eq([id])
|
|
313
|
+
expect(PWN::Sessions.list.map { |row| row[:id] }).not_to include(id)
|
|
314
|
+
lease = PWN::Sessions.activate(session_id: id)
|
|
315
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
316
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
317
|
+
result = described_class.sweep(dry_run: false)
|
|
318
|
+
expect(result[:deleted_ids]).to eq([id])
|
|
319
|
+
expect(JSON.parse(File.read(target))).to eq('unrelated' => JSON.parse(JSON.generate(valid)))
|
|
320
|
+
removal = result[:dependency_removals].first
|
|
321
|
+
expect(removal[:state]).to eq('committed')
|
|
322
|
+
expect(JSON.parse(File.read(removal[:backup]))).to have_key(key)
|
|
323
|
+
ensure
|
|
324
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it 'rejects stale map approvals and never removes a refreshed entry' do
|
|
329
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
330
|
+
target = File.join(Dir.home, '.pwn', 'memory.json')
|
|
331
|
+
key = "reflect_#{id}_abcdef12"
|
|
332
|
+
File.write(target, JSON.generate(key => 123))
|
|
333
|
+
plan = described_class.sweep
|
|
334
|
+
approvals = plan[:records].to_h { |row| [row[:id], row.slice(:files, :dependency_records)] }
|
|
335
|
+
File.write(target, JSON.generate(key => { value: 'refreshed' }))
|
|
336
|
+
expect(described_class.sweep(dry_run: false, approved_candidates: approvals)[:deleted_ids]).to eq([])
|
|
337
|
+
expect(JSON.parse(File.read(target))[key]['value']).to eq('refreshed')
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it 'protects a session while a cooperative writer can introduce a new dependency' do
|
|
341
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
342
|
+
target = File.join(Dir.home, '.pwn', 'memory.json')
|
|
343
|
+
PWN::Memory.transaction(path: target) do
|
|
344
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
345
|
+
expect(File).to exist(path)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it 'does not race a cooperating map writer' do
|
|
350
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
351
|
+
target = File.join(Dir.home, '.pwn', 'memory.json')
|
|
352
|
+
File.write(target, JSON.generate("reflect_#{id}_abcdef12" => 123))
|
|
353
|
+
PWN::Memory.transaction(path: target) do
|
|
354
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
355
|
+
end
|
|
356
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([id])
|
|
357
|
+
end
|
|
358
|
+
|
|
24
359
|
it 'protects only a held session lease and releases it while the owner remains alive' do
|
|
25
360
|
proc_root = File.join(@tmp, 'processes')
|
|
26
361
|
FileUtils.mkdir_p(proc_root)
|
|
@@ -45,12 +380,12 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
45
380
|
expect(ready_r.wait_readable(5)).not_to be_nil
|
|
46
381
|
expect(ready_r.gets).to eq("ready\n")
|
|
47
382
|
expect(described_class.legacy_owners(proc_root)).to eq([])
|
|
48
|
-
result = described_class.
|
|
383
|
+
result = described_class.sweep(dry_run: false, proc_root: proc_root)
|
|
49
384
|
expect(result[:deleted_ids]).to eq([other])
|
|
50
385
|
expect(result[:records].find { |row| row[:id] == id }[:skip]).to eq(:active)
|
|
51
386
|
stop_w.puts('release')
|
|
52
387
|
expect(ready_r.gets).to eq("released\n")
|
|
53
|
-
expect(described_class.
|
|
388
|
+
expect(described_class.sweep(dry_run: false, proc_root: proc_root)[:deleted_ids]).to eq([id])
|
|
54
389
|
ensure
|
|
55
390
|
stop_w&.close
|
|
56
391
|
ready_r&.close
|
|
@@ -80,7 +415,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
80
415
|
expect(described_class.legacy_owners(proc_root)).to eq([])
|
|
81
416
|
expect(PWN::Sessions.active?(session_id: id)).to be(true)
|
|
82
417
|
other = PWN::Sessions.create(id: '20200101_010203_abcdef13')[:id]
|
|
83
|
-
expect(described_class.
|
|
418
|
+
expect(described_class.sweep(dry_run: false, proc_root: proc_root)[:deleted_ids]).to eq([other])
|
|
84
419
|
ensure
|
|
85
420
|
PWN::Sessions.deactivate(lease: lease)
|
|
86
421
|
stop_w&.close
|
|
@@ -131,7 +466,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
131
466
|
expect(Process.wait2(child).last.exitstatus).to eq(23)
|
|
132
467
|
original.call(*args)
|
|
133
468
|
end
|
|
134
|
-
expect(described_class.
|
|
469
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([id])
|
|
135
470
|
expect { PWN::Sessions.activate(session_id: id) }.to raise_error(IOError, /not found/)
|
|
136
471
|
expect { PWN::Sessions.append(session_id: id, content: 'late writer') }.to raise_error(/not found/)
|
|
137
472
|
expect(File).not_to exist(path)
|
|
@@ -194,6 +529,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
194
529
|
before do
|
|
195
530
|
allow(Dir).to receive(:home).and_return(File.join(@tmp, 'home'))
|
|
196
531
|
FileUtils.mkdir_p(File.join(Dir.home, '.pwn'))
|
|
532
|
+
stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(Dir.home, '.pwn', 'learning.jsonl'))
|
|
197
533
|
PWN::Sessions.create(id: id)
|
|
198
534
|
end
|
|
199
535
|
|
|
@@ -201,12 +537,106 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
201
537
|
File.write(meta_path, JSON.generate({ id: id, title: title, source: 'pwn-ai' }.merge(value)))
|
|
202
538
|
end
|
|
203
539
|
|
|
540
|
+
it 'hides unretained sessions without attributing global store errors to them' do
|
|
541
|
+
File.write(File.join(Dir.home, '.pwn', 'memory.json'), '{')
|
|
542
|
+
expect(PWN::Sessions.list).to eq([])
|
|
543
|
+
row = PWN::Sessions.list(include_hidden: true).find { |session| session[:id] == id }
|
|
544
|
+
expect(row[:dependency_errors]).to eq([])
|
|
545
|
+
expect(row).not_to have_key(:unresolved_dependency_stores)
|
|
546
|
+
expect(PWN::Sessions.catalog[:unresolved_dependency_stores]).to eq([File.join(Dir.home, '.pwn', 'memory.json')])
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
it 'attributes broken learning evidence to its owner and preserves mixed valid rows' do
|
|
550
|
+
other = PWN::Sessions.create(id: '20200101_010203_abcdef13')[:id]
|
|
551
|
+
store = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
552
|
+
good = "#{JSON.generate(session_id: other, score: 1)}\n"
|
|
553
|
+
File.write(store, "#{JSON.generate(session_id: id, evidence_ref: {})}\n#{good}")
|
|
554
|
+
index = PWN::Sessions.dependency_index
|
|
555
|
+
expect(index[:errors]).to eq([])
|
|
556
|
+
expect(index[:session_errors]).to eq(id => [store])
|
|
557
|
+
expect(index[:dependencies]).to eq(other => [store])
|
|
558
|
+
expect(PWN::Sessions.list.map { |row| row[:id] }).to eq([other])
|
|
559
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
560
|
+
result = described_class.sweep(dry_run: false)
|
|
561
|
+
expect(result[:deleted_ids]).to eq([id])
|
|
562
|
+
expect(File.binread(store)).to eq(good)
|
|
563
|
+
expect(File).not_to exist(path)
|
|
564
|
+
expect(result[:dependency_removals].map { |row| row[:path] }).to eq([store])
|
|
565
|
+
expect(File.binread(result[:dependency_removals].first[:backup])).to include(id, other)
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
it 'uses a shared dependency snapshot for a dry-run inventory' do
|
|
569
|
+
4.times { |index| PWN::Sessions.create(id: "20200101_010203_abcdef2#{index}") }
|
|
570
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
571
|
+
expect(PWN::Sessions).to receive(:dependency_index).at_most(3).times.and_call_original
|
|
572
|
+
expect(described_class.sweep[:candidate_ids].length).to eq(5)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
it 'keeps active erroneous sessions physically intact but hidden' do
|
|
576
|
+
store = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
577
|
+
bytes = JSON.generate(session_id: id, evidence_ref: {})
|
|
578
|
+
File.write(store, bytes)
|
|
579
|
+
lease = PWN::Sessions.activate(session_id: id)
|
|
580
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
581
|
+
expect(PWN::Sessions.list).to eq([])
|
|
582
|
+
result = described_class.sweep(dry_run: false)
|
|
583
|
+
expect(result[:deleted_ids]).to eq([])
|
|
584
|
+
expect(result[:skipped][:active]).to eq(1)
|
|
585
|
+
expect(File.binread(store)).to eq(bytes)
|
|
586
|
+
expect(File).to exist(path)
|
|
587
|
+
ensure
|
|
588
|
+
PWN::Sessions.deactivate(lease: lease)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
it 'preserves valid map entries while reporting attributable and unresolved remediation separately' do
|
|
592
|
+
other = PWN::Sessions.create(id: '20200101_010203_abcdef13')[:id]
|
|
593
|
+
store = File.join(Dir.home, '.pwn', 'mistakes.json')
|
|
594
|
+
bytes = JSON.generate(bad: { sessions: [id], verified_sessions: {} }, good: { sessions: [other] })
|
|
595
|
+
File.write(store, bytes)
|
|
596
|
+
index = PWN::Sessions.dependency_index
|
|
597
|
+
expect(index[:session_errors]).to eq(id => [store])
|
|
598
|
+
expect(index[:dependencies]).to eq(other => [store])
|
|
599
|
+
expect(PWN::Sessions.list.map { |row| row[:id] }).to eq([other])
|
|
600
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
601
|
+
result = described_class.sweep(dry_run: false)
|
|
602
|
+
expect(result[:deleted_ids]).to eq([id])
|
|
603
|
+
expect(JSON.parse(File.binread(store))).to eq('good' => { 'sessions' => [other] })
|
|
604
|
+
expect(File.binread(result[:dependency_removals].first[:backup])).to eq(bytes)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
it 'never guesses ownership for corrupt global bytes and keeps valid summaries visible' do
|
|
608
|
+
metadata(title: 'Valid summary title', title_source: 'summary', summary: 'Conversation summary', summary_turns: 2)
|
|
609
|
+
store = File.join(Dir.home, '.pwn', 'memory.json')
|
|
610
|
+
File.write(store, '{')
|
|
611
|
+
expect(PWN::Sessions.list.map { |row| row[:id] }).to eq([id])
|
|
612
|
+
expect(PWN::Sessions.list.first[:dependency_errors]).to eq([])
|
|
613
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
614
|
+
result = described_class.sweep(dry_run: false)
|
|
615
|
+
expect(result[:unresolved_store_remediation]).to eq([store])
|
|
616
|
+
expect(result[:deleted_ids]).to eq([])
|
|
617
|
+
expect(File.read(store)).to eq('{')
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it 'rechecks invalid evidence after backup and does not remove repaired records' do
|
|
621
|
+
store = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
622
|
+
File.write(store, JSON.generate(session_id: id, evidence_ref: {}))
|
|
623
|
+
repaired = JSON.generate(session_id: id, score: 1)
|
|
624
|
+
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
625
|
+
allow(described_class).to receive(:backup).and_wrap_original do |original, *args|
|
|
626
|
+
original.call(*args)
|
|
627
|
+
File.write(store, repaired)
|
|
628
|
+
end
|
|
629
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
630
|
+
expect(File.read(store)).to eq(repaired)
|
|
631
|
+
expect(File).to exist(path)
|
|
632
|
+
end
|
|
633
|
+
|
|
204
634
|
it 'keeps referenced placeholders visible with the exact dependency paths' do
|
|
205
635
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
206
636
|
dependency = File.join(Dir.home, '.pwn', 'jobs', '123456abcdef.json')
|
|
207
637
|
FileUtils.mkdir_p(File.dirname(dependency))
|
|
208
638
|
File.write(dependency, JSON.generate(session_id: id))
|
|
209
|
-
expect(described_class.
|
|
639
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
210
640
|
row = PWN::Sessions.list.find { |session| session[:id] == id }
|
|
211
641
|
expect(row[:dependencies]).to eq([dependency])
|
|
212
642
|
expect(PWN::Sessions.resolve(identifier: id)).to eq(id)
|
|
@@ -222,7 +652,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
222
652
|
end
|
|
223
653
|
|
|
224
654
|
it 'lists an explicit empty dependency array for unreferenced sessions' do
|
|
225
|
-
expect(PWN::Sessions.list.find { |session| session[:id] == id }[:dependencies]).to eq([])
|
|
655
|
+
expect(PWN::Sessions.list(include_hidden: true).find { |session| session[:id] == id }[:dependencies]).to eq([])
|
|
226
656
|
end
|
|
227
657
|
|
|
228
658
|
it 'fails closed without reading through a symlinked artifact parent' do
|
|
@@ -234,21 +664,21 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
234
664
|
FileUtils.mkdir_p(root)
|
|
235
665
|
File.symlink(target, File.join(root, id))
|
|
236
666
|
expect(PWN::Sessions.dependency_index[:errors]).not_to be_empty
|
|
237
|
-
expect(described_class.
|
|
667
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
238
668
|
end
|
|
239
669
|
|
|
240
670
|
it 'fails closed for malformed run dependency stores' do
|
|
241
671
|
dependency = File.join(Dir.home, '.pwn', 'runs', 'fixture', 'state.json')
|
|
242
672
|
FileUtils.mkdir_p(File.dirname(dependency))
|
|
243
673
|
File.write(dependency, '{')
|
|
244
|
-
expect(described_class.
|
|
245
|
-
expect(described_class.
|
|
674
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
675
|
+
expect(described_class.sweep[:skipped][:unknown_pins]).to eq(1)
|
|
246
676
|
end
|
|
247
677
|
|
|
248
678
|
it 'never treats a placeholder as a generated title despite summary markers' do
|
|
249
679
|
metadata(title_source: 'summary', summary: 'A valid conversation summary', summary_turns: 2)
|
|
250
680
|
expect(PWN::Sessions.title_status(session_id: id)).to include(classification: :unnamed, is_summarized: false, title_reason: 'placeholder_not_summary')
|
|
251
|
-
expect(PWN::Sessions.list.find { |row| row[:id] == id }[:is_summarized]).to be(false)
|
|
681
|
+
expect(PWN::Sessions.list(include_hidden: true).find { |row| row[:id] == id }[:is_summarized]).to be(false)
|
|
252
682
|
end
|
|
253
683
|
|
|
254
684
|
it 'accepts an exact current placeholder with provisional non-summary metadata' do
|
|
@@ -294,7 +724,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
294
724
|
{ title_aliases: ['Human name'] }, { title_history: ['Prior topic'] }
|
|
295
725
|
].each do |value|
|
|
296
726
|
metadata(value)
|
|
297
|
-
row = PWN::Sessions.list.find { |session| session[:id] == id }
|
|
727
|
+
row = PWN::Sessions.list(include_hidden: true).find { |session| session[:id] == id }
|
|
298
728
|
expect(row).to include(is_summarized: false, classification: :unnamed)
|
|
299
729
|
expect(described_class.classify(path, id)).to eq(row[:classification])
|
|
300
730
|
end
|
|
@@ -348,7 +778,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
348
778
|
metadata(source: 'pwn-ai-swarm', title: 'swarm:fixture persona:critic')
|
|
349
779
|
expect(PWN::Sessions.title_status(session_id: id)).to include(classification: :unnamed, title_reason: 'missing_summary_provenance')
|
|
350
780
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
351
|
-
expect(described_class.
|
|
781
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([id])
|
|
352
782
|
end
|
|
353
783
|
|
|
354
784
|
it 'recognizes native scalar step rewards without accepting unknown reward shapes' do
|
|
@@ -379,12 +809,12 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
379
809
|
old = Time.utc(2020)
|
|
380
810
|
File.utime(old, old, path)
|
|
381
811
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
382
|
-
dry = described_class.
|
|
812
|
+
dry = described_class.sweep
|
|
383
813
|
expect(dry[:candidate_ids]).to include(id)
|
|
384
814
|
expect(dry[:deleted_ids]).to eq([])
|
|
385
815
|
expect(File).to exist(path)
|
|
386
816
|
expect(PWN::Sessions).to receive(:delete).with(session_id: id, current_session_id: nil, force: true, cleanup_lock: an_instance_of(File)).and_call_original
|
|
387
|
-
expect(described_class.
|
|
817
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([id])
|
|
388
818
|
expect(File).not_to exist(path)
|
|
389
819
|
expect(File).not_to exist(meta_path)
|
|
390
820
|
end
|
|
@@ -392,11 +822,11 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
392
822
|
it 'archives and removes hot placeholders without deleting current sessions' do
|
|
393
823
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
394
824
|
original = File.binread(path)
|
|
395
|
-
expect(described_class.
|
|
396
|
-
dry = described_class.
|
|
825
|
+
expect(described_class.sweep(dry_run: false, current_session_id: id)[:deleted_ids]).to eq([])
|
|
826
|
+
dry = described_class.sweep
|
|
397
827
|
expect(dry[:candidate_ids]).to eq([id])
|
|
398
828
|
expect(Dir.glob(File.join(Dir.home, '.pwn', 'session_cleanup_archive', '**', '*.jsonl'))).to eq([])
|
|
399
|
-
result = described_class.
|
|
829
|
+
result = described_class.sweep(dry_run: false)
|
|
400
830
|
expect(result[:deleted_ids]).to eq([id])
|
|
401
831
|
archived = File.join(result[:archive_dir], "#{id}.jsonl")
|
|
402
832
|
expect(File.binread(archived)).to eq(original)
|
|
@@ -416,6 +846,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
416
846
|
before do
|
|
417
847
|
allow(Dir).to receive(:home).and_return(File.join(@tmp, 'home'))
|
|
418
848
|
FileUtils.mkdir_p(File.join(Dir.home, '.pwn'))
|
|
849
|
+
stub_const('PWN::AI::Agent::Learning::LEARNING_FILE', File.join(Dir.home, '.pwn', 'learning.jsonl'))
|
|
419
850
|
PWN::Sessions.create(id: id)
|
|
420
851
|
end
|
|
421
852
|
|
|
@@ -423,14 +854,14 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
423
854
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
424
855
|
old = Time.utc(2020)
|
|
425
856
|
File.utime(old, old, path)
|
|
426
|
-
expect(described_class.
|
|
857
|
+
expect(described_class.sweep(dry_run: false, current_session_id: id)[:deleted_ids]).to eq([])
|
|
427
858
|
allow(described_class).to receive(:legacy_owners).and_return([{ pid: 123, reason: 'legacy_session_ownership_unknown' }])
|
|
428
|
-
result = described_class.
|
|
859
|
+
result = described_class.sweep(dry_run: false)
|
|
429
860
|
expect(result[:skipped][:unknown_legacy_ownership]).to eq(1)
|
|
430
861
|
expect(File).to exist(path)
|
|
431
862
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
432
|
-
|
|
433
|
-
expect(described_class.
|
|
863
|
+
File.write(File.join(Dir.home, '.pwn', 'learning.jsonl'), JSON.generate(session_id: id))
|
|
864
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:referenced]).to eq(1)
|
|
434
865
|
end
|
|
435
866
|
|
|
436
867
|
it 'ignores shell payload mentions while retaining actual PWN executable processes' do
|
|
@@ -469,8 +900,8 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
469
900
|
end
|
|
470
901
|
|
|
471
902
|
it 'fails closed on unknown pin stores and unavailable process inventories' do
|
|
472
|
-
|
|
473
|
-
expect(described_class.
|
|
903
|
+
File.write(File.join(Dir.home, '.pwn', 'memory.json'), '{')
|
|
904
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:unknown_pins]).to eq(1)
|
|
474
905
|
expect(described_class.legacy_owners(File.join(@tmp, 'absent'))).not_to be_empty
|
|
475
906
|
expect(File).to exist(path)
|
|
476
907
|
end
|
|
@@ -482,12 +913,12 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
482
913
|
learning = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
483
914
|
mistakes = File.join(Dir.home, '.pwn', 'mistakes.json')
|
|
484
915
|
File.write(learning, "#{JSON.generate(session_id: id, success: true, score: 1)}\n")
|
|
485
|
-
expect(described_class.
|
|
916
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:referenced]).to eq(1)
|
|
486
917
|
File.unlink(learning)
|
|
487
918
|
File.write(mistakes, JSON.generate(issue: { sessions: [id], resolved: false }))
|
|
488
|
-
expect(described_class.
|
|
919
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:referenced]).to eq(1)
|
|
489
920
|
File.write(mistakes, '{')
|
|
490
|
-
expect(described_class.
|
|
921
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:unknown_pins]).to eq(1)
|
|
491
922
|
expect(File).to exist(path)
|
|
492
923
|
end
|
|
493
924
|
|
|
@@ -503,12 +934,12 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
503
934
|
}.each do |name, data|
|
|
504
935
|
file = File.join(root, name)
|
|
505
936
|
File.write(file, data)
|
|
506
|
-
expect(described_class.
|
|
937
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:referenced]).to eq(1)
|
|
507
938
|
expect(File).to exist(path)
|
|
508
939
|
File.unlink(file)
|
|
509
940
|
end
|
|
510
941
|
File.write(File.join(root, 'memory.json'), '{')
|
|
511
|
-
expect(described_class.
|
|
942
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:unknown_pins]).to eq(1)
|
|
512
943
|
expect(File).to exist(path)
|
|
513
944
|
end
|
|
514
945
|
|
|
@@ -522,7 +953,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
522
953
|
File.write(meta_path, JSON.generate(title: 'Concurrent human title')) if calls == 2
|
|
523
954
|
original.call(*args)
|
|
524
955
|
end
|
|
525
|
-
expect(described_class.
|
|
956
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:changed]).to eq(1)
|
|
526
957
|
expect(File).to exist(path)
|
|
527
958
|
end
|
|
528
959
|
|
|
@@ -532,7 +963,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
532
963
|
original.call(*args)
|
|
533
964
|
File.open(path, 'a') { |file| file.puts(JSON.generate(role: 'user', content: 'concurrent append')) }
|
|
534
965
|
end
|
|
535
|
-
result = described_class.
|
|
966
|
+
result = described_class.sweep(dry_run: false)
|
|
536
967
|
expect(result[:deleted_ids]).to eq([])
|
|
537
968
|
expect(result[:skipped][:changed]).to eq(1)
|
|
538
969
|
expect(File).to exist(path)
|
|
@@ -541,13 +972,13 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
541
972
|
it 'refuses deletion on an archive failure or newly created evidence reference' do
|
|
542
973
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
543
974
|
allow(described_class).to receive(:backup).and_raise(IOError)
|
|
544
|
-
expect(described_class.
|
|
975
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([])
|
|
545
976
|
expect(File).to exist(path)
|
|
546
977
|
allow(described_class).to receive(:backup).and_wrap_original do |original, *args|
|
|
547
978
|
original.call(*args)
|
|
548
979
|
File.write(File.join(Dir.home, '.pwn', 'learning.jsonl'), JSON.generate(session_id: id))
|
|
549
980
|
end
|
|
550
|
-
expect(described_class.
|
|
981
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:changed]).to eq(1)
|
|
551
982
|
expect(File).to exist(path)
|
|
552
983
|
end
|
|
553
984
|
|
|
@@ -562,10 +993,10 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
562
993
|
end
|
|
563
994
|
dependency = File.join(Dir.home, '.pwn', 'learning.jsonl')
|
|
564
995
|
File.write(dependency, JSON.generate(session_id: kept, details: "A prose mention of #{id}"))
|
|
565
|
-
dry = described_class.
|
|
996
|
+
dry = described_class.sweep
|
|
566
997
|
expect(dry[:candidate_ids]).to eq([id])
|
|
567
998
|
expect(dry[:deleted_ids]).to eq([])
|
|
568
|
-
expect(described_class.
|
|
999
|
+
expect(described_class.sweep(dry_run: false)[:deleted_ids]).to eq([id])
|
|
569
1000
|
rows = PWN::Sessions.list
|
|
570
1001
|
expect(rows.map { |row| row[:id] }).to contain_exactly(kept, '20200101_010203_abcdef14')
|
|
571
1002
|
expect(rows.find { |row| row[:id] == kept }[:dependencies]).to eq([dependency])
|
|
@@ -575,7 +1006,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
575
1006
|
it 'never puts transcript bodies into its dry run ledger' do
|
|
576
1007
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
577
1008
|
File.open(path, 'a') { |file| file.puts(JSON.generate(role: 'user', content: 'private body sentinel')) }
|
|
578
|
-
result = described_class.
|
|
1009
|
+
result = described_class.sweep
|
|
579
1010
|
expect(JSON.generate(result)).not_to include('private body sentinel')
|
|
580
1011
|
expect(result[:records].first[:files]["#{id}.jsonl"][:sha256]).to eq(Digest::SHA256.file(path).hexdigest)
|
|
581
1012
|
end
|
|
@@ -586,7 +1017,7 @@ RSpec.describe PWN::SessionCleanup do
|
|
|
586
1017
|
allow(described_class).to receive(:legacy_owners).and_return([])
|
|
587
1018
|
File.open(path) do |file|
|
|
588
1019
|
file.flock(File::LOCK_EX)
|
|
589
|
-
expect(described_class.
|
|
1020
|
+
expect(described_class.sweep(dry_run: false)[:skipped][:locked]).to eq(1)
|
|
590
1021
|
end
|
|
591
1022
|
expect(File).to exist(path)
|
|
592
1023
|
end
|