pwn 0.5.685 → 0.5.686

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.
@@ -633,12 +633,42 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
633
633
  expect(described_class).not_to respond_to :request_kind
634
634
  end
635
635
 
636
- it 'run does not short-circuit statement/question — they take the full tool loop' do
636
+ it 'does not invent a request type for world-knowledge; the full loop still accepts a text final' do
637
+ expect(described_class.world_knowledge?(request: 'what color is a passion fruit?')).to eq true
637
638
  src = File.read(described_class.method(:run).source_location.first)
638
- expect(src).not_to match(/kind\.to_sym == :statement/)
639
- expect(src).not_to match(/kind\.to_sym == :question/)
640
- expect(src).not_to match(/Request type/)
641
- expect(src).to match(/Every request gets a task compass/)
639
+ expect(src).not_to match(/request_kind/)
640
+ expect(src).not_to match(/world_knowledge\?.*?return answer_question/m)
641
+ expect(src).to match(/world_knowledge\?/)
642
+ end
643
+
644
+ it 'full tool loop accepts a text final for world-knowledge with no host-work tools' do
645
+ expect(described_class.world_knowledge?(request: 'what color is a passion fruit?')).to eq true
646
+ expect(described_class.send(:request_need, request: 'what color is a passion fruit?')).to eq :none
647
+ expect(
648
+ described_class.send(
649
+ :request_unsatisfied?,
650
+ request: 'what color is a passion fruit?',
651
+ messages: [{ role: 'assistant', content: 'Purple when ripe.', tool_calls: [] }]
652
+ )
653
+ ).to eq false
654
+ sess_tmp = Dir.mktmpdir('pwn-sess-fruit')
655
+ stub_const('PWN::Sessions::SESSIONS_DIR', sess_tmp)
656
+ sid = 'fruit_spec'
657
+ PWN::Sessions.create(id: sid, title: 'fruit')
658
+ allow(described_class).to receive(:should_auto_introspect?).and_return(false)
659
+ allow(described_class).to receive(:plan_first).and_raise('plan_first must not run on world-knowledge')
660
+ allow(PWN::AI::Agent::TaskSummarizer).to receive(:enabled?).and_return(false) if defined?(PWN::AI::Agent::TaskSummarizer)
661
+ allow(described_class).to receive(:call_engine).and_return(
662
+ { role: 'assistant', content: 'Purple when ripe.', tool_calls: [] }
663
+ )
664
+ out = described_class.run(
665
+ request: 'what color is a passion fruit?',
666
+ session_id: sid,
667
+ system_role_content: 'test system'
668
+ )
669
+ expect(out).to include('Purple when ripe.')
670
+ ensure
671
+ FileUtils.rm_rf(sess_tmp) if sess_tmp
642
672
  end
643
673
 
644
674
  it 'classifies pure prior-turn recall and vague memory cues as :recall' do
@@ -979,4 +1009,12 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
979
1009
  expect(src).not_to include('return false if kind == :statement')
980
1010
  expect(src).not_to include('return false if kind == :question && fails.zero?')
981
1011
  end
1012
+
1013
+ it 'streams request-processing progress through PWN::Plugins::Log when debug is on' do
1014
+ src = File.read(described_class.method(:run).source_location.first)
1015
+ expect(src).to match(/PWN::Plugins::Log\.start_debug/)
1016
+ expect(src).to match(/PWN::Plugins::Log\.progress/)
1017
+ expect(src).to match(/quiet_debug_tui!/)
1018
+ expect(described_class).to respond_to(:debug_on?)
1019
+ end
982
1020
  end # rubocop:enable Metrics/BlockLength
@@ -47,8 +47,8 @@ describe PWN::AI::Anthropic do
47
47
  end
48
48
  end
49
49
 
50
- it 'exposes get_plan_usage for the PS1 subscription suffix' do
51
- expect(described_class).to respond_to(:get_plan_usage)
50
+ it 'does not expose get_plan_usage' do
51
+ expect(described_class).not_to respond_to(:get_plan_usage)
52
52
  end
53
53
 
54
54
  describe 'Hermes prompt-cache breakpoint' do
@@ -13,8 +13,8 @@ describe PWN::AI::Gemini do
13
13
  expect(help_response).to respond_to :help
14
14
  end
15
15
 
16
- it 'exposes get_plan_usage for the PS1 subscription suffix' do
17
- expect(described_class).to respond_to(:get_plan_usage)
16
+ it 'does not expose get_plan_usage' do
17
+ expect(described_class).not_to respond_to(:get_plan_usage)
18
18
  end
19
19
 
20
20
  it 'chat_with_tools honors PromptCache when prompt_cache is on' do
@@ -18,8 +18,8 @@ describe PWN::AI::Grok do
18
18
  expect(src).to match(/openai_wire_messages/)
19
19
  end
20
20
 
21
- it 'exposes get_plan_usage for the PS1 subscription suffix' do
22
- expect(described_class).to respond_to(:get_plan_usage)
21
+ it 'does not expose get_plan_usage' do
22
+ expect(described_class).not_to respond_to(:get_plan_usage)
23
23
  end
24
24
 
25
25
  it 'grok_rest_call swallows ReadTimeout quietly without ERROR: print' do
@@ -321,12 +321,7 @@ describe PWN::AI::Ollama do
321
321
  expect(args[:command] || args['command']).to eq('id')
322
322
  end
323
323
 
324
- it 'exposes get_plan_usage for the PS1 subscription suffix' do
325
- expect(described_class).to respond_to(:get_plan_usage)
326
- end
327
- it 'marks local plan usage as unlimited so the PS1 shows infinity' do
328
- usage = described_class.get_plan_usage
329
- expect(usage[:available]).to be false
330
- expect(usage[:unlimited]).to be true
324
+ it 'does not expose get_plan_usage' do
325
+ expect(described_class).not_to respond_to(:get_plan_usage)
331
326
  end
332
327
  end
@@ -18,29 +18,8 @@ describe PWN::AI::OpenAI do
18
18
  expect(src).to match(/openai_wire_messages/)
19
19
  end
20
20
 
21
- it 'exposes get_plan_usage for the PS1 subscription suffix' do
22
- expect(described_class).to respond_to(:get_plan_usage)
23
- end
24
- it 'computes a percentage from dashboard billing used vs hard limit' do
25
- allow(described_class).to receive(:plan_usage_credentials?).and_return(true)
26
- allow(described_class).to receive(:open_ai_rest_call) do |**kwargs|
27
- case kwargs[:rest_call]
28
- when 'dashboard/billing/subscription'
29
- '{"hard_limit_usd":100.0}'
30
- when 'dashboard/billing/usage'
31
- '{"total_usage":1900}'
32
- else
33
- '{"error":"no"}'
34
- end
35
- end
36
- usage = described_class.get_plan_usage
37
- expect(usage[:available]).to be true
38
- expect(usage[:percent]).to eq(19)
39
- end
40
-
41
- it 'returns unavailable when credentials are missing' do
42
- allow(described_class).to receive(:plan_usage_credentials?).and_return(false)
43
- expect(described_class.get_plan_usage[:available]).to be false
21
+ it 'does not expose get_plan_usage' do
22
+ expect(described_class).not_to respond_to(:get_plan_usage)
44
23
  end
45
24
 
46
25
  it 'open_ai_rest_call swallows ReadTimeout quietly without ERROR: print' do
@@ -300,12 +300,7 @@ describe PWN::AI::OpenWebUI do
300
300
  expect(args[:command] || args['command']).to eq('id')
301
301
  end
302
302
 
303
- it 'exposes get_plan_usage for the PS1 subscription suffix' do
304
- expect(described_class).to respond_to(:get_plan_usage)
305
- end
306
- it 'marks local plan usage as unlimited so the PS1 shows infinity' do
307
- usage = described_class.get_plan_usage
308
- expect(usage[:available]).to be false
309
- expect(usage[:unlimited]).to be true
303
+ it 'does not expose get_plan_usage' do
304
+ expect(described_class).not_to respond_to(:get_plan_usage)
310
305
  end
311
306
  end
@@ -8,26 +8,9 @@ describe PWN::AI do
8
8
  expect(help_response).not_to be_nil
9
9
  end
10
10
 
11
- it 'normalizes used/limit into a clamped percentage' do
12
- usage = described_class.normalize_plan_usage(used: 19, limit: 100, engine: :grok)
13
- expect(usage[:available]).to be true
14
- expect(usage[:percent]).to eq(19)
15
- end
16
-
17
- it 'returns the infinity glyph for ollama and openwebui' do
18
- expect(described_class.plan_usage_glyph(engine: :ollama)).to eq("\u221E")
19
- expect(described_class.plan_usage_glyph(engine: :openwebui)).to eq("\u221E")
20
- end
21
-
22
- it 'returns a percent glyph when usage data is available' do
23
- glyph = described_class.plan_usage_glyph(
24
- engine: :grok,
25
- usage: { available: true, percent: 19 }
26
- )
27
- expect(glyph).to eq('19%')
28
- end
29
-
30
- it 'returns infinity when a hosted engine has no usage data' do
31
- expect(described_class.plan_usage_glyph(engine: :gemini, usage: { available: false })).to eq("\u221E")
11
+ it 'does not expose plan-usage helpers' do
12
+ expect(described_class).not_to respond_to(:normalize_plan_usage)
13
+ expect(described_class).not_to respond_to(:plan_usage)
14
+ expect(described_class).not_to respond_to(:plan_usage_glyph)
32
15
  end
33
16
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spec_helper'
4
+ require 'stringio'
4
5
 
5
6
  describe PWN::Plugins::Log do
6
7
  it 'should display information for authors' do
@@ -12,4 +13,142 @@ describe PWN::Plugins::Log do
12
13
  help_response = PWN::Plugins::Log
13
14
  expect(help_response).to respond_to :help
14
15
  end
16
+
17
+ describe 'pwn-ai debug session' do
18
+ after do
19
+ PWN::Plugins::Log.stop_debug if PWN::Plugins::Log.respond_to?(:stop_debug)
20
+ end
21
+
22
+ it 'starts a /tmp/pwn-ai-DEBUG-TIMESTAMP.log and tees progress to TUI + file' do
23
+ expect(PWN::Plugins::Log).to respond_to(:start_debug)
24
+ expect(PWN::Plugins::Log).to respond_to(:stop_debug)
25
+ expect(PWN::Plugins::Log).to respond_to(:progress)
26
+ expect(PWN::Plugins::Log).to respond_to(:debug_enabled?)
27
+ expect(PWN::Plugins::Log).to respond_to(:debug_log_path)
28
+
29
+ tee = StringIO.new
30
+ path = PWN::Plugins::Log.start_debug(tee: tee, path: nil)
31
+ expect(path).to match(%r{\A/tmp/pwn-ai-DEBUG-.+\.log\z})
32
+ expect(File.file?(path)).to eq(true)
33
+ expect(PWN::Plugins::Log.debug_enabled?).to eq(true)
34
+ expect(PWN::Plugins::Log.debug_log_path).to eq(path)
35
+
36
+ ok = PWN::Plugins::Log.progress(msg: 'Loop.run start request=hi', which_self: PWN::AI::Agent::Loop)
37
+ expect(ok).to eq(true)
38
+ body = File.read(path)
39
+ expect(body).to include('Loop.run start request=hi')
40
+ expect(body).to include('PWN::AI::Agent::Loop')
41
+ expect(tee.string).to include('Loop.run start request=hi')
42
+
43
+ PWN::Plugins::Log.stop_debug
44
+ expect(PWN::Plugins::Log.debug_enabled?).to eq(false)
45
+ expect(PWN::Plugins::Log.progress(msg: 'after stop', which_self: self)).to eq(false)
46
+ end
47
+
48
+ it 'quiet_tui keeps writing the file but stops teeing to the TUI' do
49
+ expect(PWN::Plugins::Log).to respond_to(:quiet_tui!)
50
+ expect(PWN::Plugins::Log).to respond_to(:loud_tui!)
51
+ tee = StringIO.new
52
+ path = PWN::Plugins::Log.start_debug(tee: tee, path: "/tmp/pwn-ai-DEBUG-#{Process.pid}-quiet.log")
53
+ PWN::Plugins::Log.progress(msg: 'before-quiet', which_self: PWN::AI::Agent::Loop)
54
+ PWN::Plugins::Log.quiet_tui!
55
+ PWN::Plugins::Log.progress(msg: 'after-quiet', which_self: PWN::AI::Agent::Loop)
56
+ expect(tee.string).to include('before-quiet')
57
+ expect(tee.string).not_to include('after-quiet')
58
+ expect(File.read(path)).to include('after-quiet')
59
+ PWN::Plugins::Log.loud_tui!
60
+ PWN::Plugins::Log.progress(msg: 'after-loud', which_self: PWN::AI::Agent::Loop)
61
+ expect(tee.string).to include('after-loud')
62
+ end
63
+
64
+ it 'rolls the debug log to .N+1 when the file exceeds 1MB' do
65
+ stem = "/tmp/pwn-ai-DEBUG-#{Process.pid}-roll"
66
+ first = "#{stem}.1.log"
67
+ File.write(first, 'x' * (1_024_000 + 10))
68
+ path = PWN::Plugins::Log.start_debug(tee: StringIO.new, path: first)
69
+ expect(path).to eq(first)
70
+ PWN::Plugins::Log.progress(msg: 'overflow-line', which_self: PWN::AI::Agent::Loop)
71
+ second = "#{stem}.2.log"
72
+ expect(PWN::Plugins::Log.debug_log_path).to eq(second)
73
+ expect(File.file?(second)).to eq(true)
74
+ expect(File.read(second)).to include('overflow-line')
75
+ end
76
+
77
+ it 'traces PWN module calls into the same debug log while the session is on' do
78
+ path = PWN::Plugins::Log.start_debug(tee: StringIO.new, path: "/tmp/pwn-ai-DEBUG-#{Process.pid}-#{Time.now.to_i}.log")
79
+ PWN::AI::Agent::OpenGoal.current if defined?(PWN::AI::Agent::OpenGoal)
80
+ body = File.read(path)
81
+ expect(body).to match(/PWN::AI::Agent::OpenGoal/)
82
+ end
83
+
84
+ it 'does not raise when a traced module #name is a Symbol' do
85
+ probe = Module.new do
86
+ def self.name
87
+ :symname
88
+ end
89
+
90
+ def self.ping
91
+ :ok
92
+ end
93
+ end
94
+ stub_const('PWN::AI::Agent::SymNameProbe', probe)
95
+ PWN::Plugins::Log.start_debug(
96
+ tee: StringIO.new,
97
+ path: "/tmp/pwn-ai-DEBUG-#{Process.pid}-sym.log"
98
+ )
99
+ expect { PWN::AI::Agent::SymNameProbe.ping }.not_to raise_error
100
+ end
101
+
102
+ it 'logs the complete traced call including parameters' do
103
+ probe = Module.new do
104
+ def self.echo(opts = {})
105
+ opts[:msg]
106
+ end
107
+ end
108
+ stub_const('PWN::AI::Agent::ArgProbe', probe)
109
+ path = PWN::Plugins::Log.start_debug(
110
+ tee: StringIO.new,
111
+ path: "/tmp/pwn-ai-DEBUG-#{Process.pid}-args.log"
112
+ )
113
+ PWN::AI::Agent::ArgProbe.echo(
114
+ msg: 'hello-browser',
115
+ url: 'https://example.test/open',
116
+ password: 's3cret-token'
117
+ )
118
+ body = File.read(path)
119
+ expect(body).to include('PWN::AI::Agent::ArgProbe.echo')
120
+ expect(body).to include('hello-browser')
121
+ expect(body).to include('https://example.test/open')
122
+ expect(body).not_to include('s3cret-token')
123
+ expect(body).to match(/\[REDACTED\]/)
124
+ end
125
+
126
+ it 'redacts secret-shaped values even when the key is not a password name' do
127
+ probe = Module.new do
128
+ def self.echo(opts = {})
129
+ opts[:msg]
130
+ end
131
+ end
132
+ stub_const('PWN::AI::Agent::ArgProbe', probe)
133
+ path = PWN::Plugins::Log.start_debug(
134
+ tee: StringIO.new,
135
+ path: "/tmp/pwn-ai-DEBUG-#{Process.pid}-redact.log"
136
+ )
137
+ PWN::AI::Agent::ArgProbe.echo(
138
+ header: 'Bearer eyJhbGciOiJIUzI1NiJ9.aaa.bbb',
139
+ blob: "-----BEGIN PRIVATE KEY-----\nMIIB\n-----END PRIVATE KEY-----",
140
+ note: 'xai-abcdefghijklmnopqrstuvwxyz0123456789ABCD'
141
+ )
142
+ PWN::Plugins::Log.progress(
143
+ msg: 'Loop.run start request=use key sk-live-SUPERSECRET999',
144
+ which_self: PWN::AI::Agent::Loop
145
+ )
146
+ body = File.read(path)
147
+ expect(body).not_to include('eyJhbGciOiJIUzI1NiJ9')
148
+ expect(body).not_to include('BEGIN PRIVATE KEY')
149
+ expect(body).not_to include('xai-abcdefghijklmnopqrstuvwxyz0123456789ABCD')
150
+ expect(body).not_to include('sk-live-SUPERSECRET999')
151
+ expect(body).to match(/\[REDACTED\]/)
152
+ end
153
+ end
15
154
  end
@@ -132,3 +132,34 @@ describe 'pwn-vault Config RuntimeError retry' do
132
132
  end
133
133
  end
134
134
  end
135
+
136
+ describe 'toggle-pwn-ai-debug' do
137
+ let(:repl_src) { File.read(PWN::Plugins::REPL.method(:add_commands).source_location.first) }
138
+ let(:block) do
139
+ start = repl_src.index("Pry::Commands.create_command 'toggle-pwn-ai-debug'")
140
+ stop = repl_src.index("Pry::Commands.create_command 'toggle-pwn-ai-speaks'")
141
+ repl_src[start...stop]
142
+ end
143
+
144
+ it 'starts and stops PWN::Plugins::Log debug session on the TUI toggle' do
145
+ expect(block).to include('PWN::Plugins::Log.start_debug')
146
+ expect(block).to include('PWN::Plugins::Log.stop_debug')
147
+ expect(block).to include('pwn-ai-DEBUG')
148
+ end
149
+ end
150
+
151
+ describe 'pwn-ai debug final answer placement' do
152
+ let(:repl_src) { File.read(PWN::Plugins::REPL.method(:add_commands).source_location.first) }
153
+ let(:hook) do
154
+ start = repl_src.index('Pry.config.hooks.add_hook(:after_read, :pwn_ai_hook)')
155
+ repl_src[start..]
156
+ end
157
+
158
+ it 'prints the green final after Loop.run and does not pp the session after it' do
159
+ expect(hook).to match(/Loop\.run\(/)
160
+ expect(hook).to match(/\\e\[32m\\002#\{final\}/)
161
+ native = hook[hook.index('final = PWN::AI::Agent::Loop.run')..hook.index("request.replace('nil')")]
162
+ expect(native).not_to match(/pp PWN::Sessions\.load/)
163
+ expect(native).to match(/\$stdout\.flush/)
164
+ end
165
+ end
@@ -13,10 +13,11 @@ describe PWN::Plugins::REPL do
13
13
  expect(help_response).to respond_to :help
14
14
  end
15
15
 
16
- it 'appends plan usage after the token budget in the pwn.ai PS1' do
16
+ it 'does not append plan usage to the pwn.ai PS1' do
17
17
  src = File.read(described_class.method(:refresh_ps1_proc).source_location.first)
18
- expect(src).to include('PWN::AI.plan_usage_glyph(engine: engine)')
19
- expect(src).to include("\#{current_context_length}/\#{plan_usage_glyph}")
18
+ expect(src).not_to include('plan_usage_glyph')
19
+ expect(src).not_to include('PWN::AI.plan_usage')
20
+ expect(src).to include('current_context_length')
20
21
  end
21
22
 
22
23
  it 'formats compact token counts for the PS1 budget' do