scout-ai 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.vimproject +138 -50
- data/README.md +171 -290
- data/Rakefile +17 -1
- data/VERSION +1 -1
- data/doc/Improvements.md +325 -0
- data/doc/StartHere.md +110 -0
- data/doc/developer/Architecture.md +126 -0
- data/doc/developer/Backends.md +199 -0
- data/doc/developer/ChatLifecycle.md +183 -0
- data/doc/developer/DelegationInternals.md +295 -0
- data/doc/developer/DesignPrinciples.md +245 -0
- data/doc/developer/PromptProcessing.md +292 -0
- data/doc/developer/Provenance.md +317 -0
- data/doc/user/BuildingAgents.md +345 -0
- data/doc/user/Cookbook.md +333 -0
- data/doc/user/CoreConcepts.md +181 -0
- data/doc/user/Delegation.md +191 -0
- data/doc/user/GettingStarted.md +159 -0
- data/doc/user/ManagingContext.md +163 -0
- data/doc/user/MultiAgentWorkflows.md +256 -0
- data/doc/user/Python.md +159 -0
- data/doc/user/RunningInference.md +200 -0
- data/doc/user/ToolCalling.md +193 -0
- data/doc/user/WritingChats.md +197 -0
- data/lib/scout/llm/agent/chat.rb +61 -11
- data/lib/scout/llm/agent/delegate.rb +274 -65
- data/lib/scout/llm/agent/iterate.rb +2 -2
- data/lib/scout/llm/agent/save.rb +273 -0
- data/lib/scout/llm/agent/workflow.rb +164 -0
- data/lib/scout/llm/agent.rb +86 -61
- data/lib/scout/llm/ask.rb +62 -17
- data/lib/scout/llm/backends/anthropic.rb +9 -2
- data/lib/scout/llm/backends/bedrock.rb +15 -3
- data/lib/scout/llm/backends/default.rb +183 -99
- data/lib/scout/llm/backends/glm.rb +58 -0
- data/lib/scout/llm/backends/huggingface.rb +196 -26
- data/lib/scout/llm/backends/ollama.rb +13 -1
- data/lib/scout/llm/backends/openai.rb +0 -2
- data/lib/scout/llm/backends/openwebui.rb +20 -13
- data/lib/scout/llm/backends/relay.rb +22 -22
- data/lib/scout/llm/backends/responses.rb +1 -1
- data/lib/scout/llm/chat/agent_meta.rb +264 -0
- data/lib/scout/llm/chat/annotation.rb +39 -10
- data/lib/scout/llm/chat/parse.rb +28 -6
- data/lib/scout/llm/chat/persist.rb +25 -0
- data/lib/scout/llm/chat/process/clear.rb +41 -6
- data/lib/scout/llm/chat/process/files.rb +21 -6
- data/lib/scout/llm/chat/process/meta.rb +421 -34
- data/lib/scout/llm/chat/process/options.rb +21 -1
- data/lib/scout/llm/chat/process/tools.rb +56 -15
- data/lib/scout/llm/chat/process.rb +4 -0
- data/lib/scout/llm/chat/prompt/shorten_tools.rb +125 -0
- data/lib/scout/llm/chat/prompt/shorten_tools_epoch.rb +365 -0
- data/lib/scout/llm/chat/prompt.rb +48 -0
- data/lib/scout/llm/chat/provenance.rb +775 -0
- data/lib/scout/llm/chat/tool_calls.rb +76 -0
- data/lib/scout/llm/chat.rb +18 -2
- data/lib/scout/llm/embed.rb +11 -3
- data/lib/scout/llm/image.rb +86 -0
- data/lib/scout/llm/mcp.rb +10 -2
- data/lib/scout/llm/rag.rb +3 -3
- data/lib/scout/llm/tools/call.rb +160 -11
- data/lib/scout/llm/tools/knowledge_base.rb +1 -1
- data/lib/scout/llm/tools/workflow.rb +32 -16
- data/lib/scout/model/python/huggingface/causal.rb +23 -5
- data/lib/scout/model/python/huggingface.rb +2 -1
- data/lib/scout-ai.rb +1 -0
- data/python/README.md +197 -14
- data/python/scout_ai/huggingface/eval.py +245 -34
- data/python/tests/test_huggingface_eval.py +58 -0
- data/research/ChatAnalyst-required-changes.md +167 -0
- data/research/agent-delegation-analysis.md +810 -0
- data/research/agent-meta-provenance-integration-plan.md +622 -0
- data/research/agent-workflow-analysis.md +1120 -0
- data/research/backends-analysis.md +836 -0
- data/research/chat-core-analysis.md +946 -0
- data/research/chatanalyst-provenance/00-baseline.md +30 -0
- data/research/chatanalyst-provenance/01-repo-map.md +60 -0
- data/research/chatanalyst-provenance/02-event-reconstruction.md +55 -0
- data/research/chatanalyst-provenance/03-duplication-evidence.md +45 -0
- data/research/chatanalyst-provenance/04-tooling-root-cause.md +57 -0
- data/research/chatanalyst-provenance/05-fix-plan.md +46 -0
- data/research/chatanalyst-provenance/07-critic-review.md +25 -0
- data/research/chatanalyst-provenance/final-report.md +45 -0
- data/research/chatanalyst-provenance/resumption.md +37 -0
- data/research/coding-philosophy-analysis.md +928 -0
- data/research/commands-analysis.md +947 -0
- data/research/multi-agent-patterns-analysis.md +853 -0
- data/research/prompt-strategies-analysis.md +630 -0
- data/research/prov-verbosity-fix-notes.md +77 -0
- data/research/provenance-analysis.md +469 -0
- data/research/provenance-navigation-design.md +640 -0
- data/research/synthesis-report.md +487 -0
- data/research/tools-system-analysis.md +779 -0
- data/scout-ai.gemspec +100 -11
- data/scout_commands/agent/ask +13 -3
- data/scout_commands/agent/kb +2 -0
- data/scout_commands/llm/ask +11 -4
- data/scout_commands/llm/md +76 -0
- data/scout_commands/llm/process_queries +48 -0
- data/scout_commands/llm/prov +602 -0
- data/scout_commands/llm/word +71 -0
- data/scout_commands/workflow/mcp +43 -0
- data/share/word/reference.docx +0 -0
- data/test/etc/AI/mock.yaml +11 -0
- data/test/fixtures/backends/anthropic.json +19 -0
- data/test/fixtures/backends/anthropic_tool_use.json +24 -0
- data/test/fixtures/backends/bedrock.json +8 -0
- data/test/fixtures/backends/bedrock_embedding.json +3 -0
- data/test/fixtures/backends/bedrock_tool_use.json +17 -0
- data/test/fixtures/backends/ollama.json +16 -0
- data/test/fixtures/backends/ollama_tool_call.json +27 -0
- data/test/fixtures/backends/openai_chat.json +21 -0
- data/test/fixtures/backends/openai_chat_tool_call.json +31 -0
- data/test/fixtures/backends/responses.json +33 -0
- data/test/fixtures/backends/responses_tool_call.json +28 -0
- data/test/integration/README.md +32 -0
- data/test/integration/scout/llm/backends/test_endpoints.rb +34 -0
- data/test/integration/scout/llm/backends/test_openwebui.rb +61 -0
- data/test/integration/scout/llm/backends/test_relay.rb +52 -0
- data/test/integration/scout/llm/test_infrastructure.rb +74 -0
- data/test/{scout → integration/scout}/llm/test_mcp.rb +1 -1
- data/test/integration/scout/llm/tools/test_mcp.rb +42 -0
- data/test/integration/scout/model/test_base.rb +91 -0
- data/test/scout/llm/agent/test_chat.rb +8 -2
- data/test/scout/llm/agent/test_save.rb +413 -0
- data/test/scout/llm/agent/test_workflow.rb +110 -0
- data/test/scout/llm/backends/test_anthropic.rb +93 -10
- data/test/scout/llm/backends/test_bedrock.rb +118 -2
- data/test/scout/llm/backends/test_huggingface.rb +137 -42
- data/test/scout/llm/backends/test_ollama.rb +70 -20
- data/test/scout/llm/backends/test_openwebui.rb +42 -40
- data/test/scout/llm/backends/test_relay.rb +4 -2
- data/test/scout/llm/chat/agent_meta_fixtures.rb +131 -0
- data/test/scout/llm/chat/process/test_meta.rb +518 -0
- data/test/scout/llm/chat/process/test_normalize_usage.rb +183 -0
- data/test/scout/llm/chat/test_agent_meta.rb +357 -0
- data/test/scout/llm/chat/test_agent_meta_provenance.rb +467 -0
- data/test/scout/llm/chat/test_agent_meta_tokens.rb +594 -0
- data/test/scout/llm/chat/test_parse.rb +70 -15
- data/test/scout/llm/chat/test_prov_cli.rb +274 -0
- data/test/scout/llm/chat/test_provenance.rb +240 -0
- data/test/scout/llm/chat/test_tool_calls.rb +38 -0
- data/test/scout/llm/test_agent.rb +13 -36
- data/test/scout/llm/test_ask.rb +75 -52
- data/test/scout/llm/test_chat.rb +107 -13
- data/test/scout/llm/test_embed.rb +48 -0
- data/test/scout/llm/test_rag.rb +23 -16
- data/test/scout/llm/test_tools.rb +12 -1
- data/test/scout/llm/tools/test_knowledge_base.rb +0 -1
- data/test/scout/llm/tools/test_mcp.rb +5 -3
- data/test/scout/llm/tools/test_workflow.rb +23 -2
- data/test/scout/model/python/huggingface/causal/test_next_token.rb +11 -5
- data/test/scout/model/python/huggingface/test_causal.rb +9 -3
- data/test/scout/model/python/huggingface/test_classification.rb +11 -2
- data/test/scout/model/python/test_torch.rb +2 -0
- data/test/scout/model/python/torch/test_helpers.rb +4 -0
- data/test/scout/model/test_base.rb +4 -2
- data/test/support/availability.rb +231 -0
- data/test/support/fake_clients.rb +138 -0
- data/test/support/fixtures.rb +21 -0
- data/test/support/infrastructure_probes.rb +136 -0
- data/test/support/mock_backend.rb +215 -0
- data/test/test_helper.rb +32 -2
- metadata +99 -10
- data/doc/Agent.md +0 -327
- data/doc/Chat.md +0 -458
- data/doc/LLM.md +0 -340
- data/doc/RAG.md +0 -129
- data/scout_commands/documenter +0 -148
- data/test/scout/llm/backends/test_openai.rb +0 -192
- data/test/scout/llm/backends/test_responses.rb +0 -238
- data/test/scout/llm/test_parse.rb +0 -98
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
|
+
require 'scout/llm/chat'
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'agent_meta_fixtures'
|
|
5
|
+
|
|
6
|
+
# Token accounting for agent_meta receipts (plan delivery step 3): the
|
|
7
|
+
# provenance-aware event collector. Fixtures A, B, D, F, G and H from the
|
|
8
|
+
# design record; traversal structure itself is covered by
|
|
9
|
+
# test_agent_meta_provenance.rb.
|
|
10
|
+
class TestChatAgentMetaTokens < Test::Unit::TestCase
|
|
11
|
+
include AgentMetaFixtures
|
|
12
|
+
|
|
13
|
+
## Fixture A - receipt-only delegation
|
|
14
|
+
|
|
15
|
+
def test_receipt_only_events_are_counted_once
|
|
16
|
+
TmpFile.with_dir do |dir|
|
|
17
|
+
parent = write_chat(dir, 'parent.chat',
|
|
18
|
+
receipt_chat_text(
|
|
19
|
+
{'a1' => [meta_receipt('pt=100 ct=50 tt=150 inference_id=w1'),
|
|
20
|
+
meta_receipt('pt=20 ct=10 tt=30 inference_id=w2')]},
|
|
21
|
+
extra: ['meta: pt=60 ct=40 tt=100 inference_id=p1']
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
events = Chat.provenance_token_events(parent)
|
|
25
|
+
|
|
26
|
+
# Both worker events plus the parent's own direct event.
|
|
27
|
+
assert_equal %w[p1 w1 w2], events.collect { |event| event[:inference_id] }.sort
|
|
28
|
+
|
|
29
|
+
worker = events.select { |event| %w[w1 w2].include?(event[:inference_id]) }
|
|
30
|
+
assert_equal [20, 100], worker.collect { |event| event[:tokens][:pt] }.sort
|
|
31
|
+
assert worker.all? { |event| event[:deduplication] == :inference_id }
|
|
32
|
+
assert worker.all? { |event| !event[:conflict] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_receipt_only_evidence_addresses_and_call_ids
|
|
37
|
+
TmpFile.with_dir do |dir|
|
|
38
|
+
parent = write_chat(dir, 'parent.chat',
|
|
39
|
+
receipt_chat_text(
|
|
40
|
+
{'a1' => [meta_receipt('pt=100 ct=50 tt=150 inference_id=w1'),
|
|
41
|
+
meta_receipt('pt=20 ct=10 tt=30 inference_id=w2')]}
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
evidence = Chat.provenance_token_events(parent)
|
|
45
|
+
.sort_by { |event| event[:inference_id] }
|
|
46
|
+
.collect { |event| event[:evidence].first }
|
|
47
|
+
|
|
48
|
+
assert_equal [parent, 2, :agent_meta, 0], evidence.first[:evidence_address]
|
|
49
|
+
assert_equal [parent, 2, :agent_meta, 1], evidence.last[:evidence_address]
|
|
50
|
+
assert_equal %w[a1 a1], evidence.collect { |record| record[:call_id] }
|
|
51
|
+
assert_equal %w[ask ask], evidence.collect { |record| record[:tool_name] }
|
|
52
|
+
assert_equal parent, evidence.first[:source]
|
|
53
|
+
assert_equal :agent_meta, evidence.first[:origin]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_receipt_only_scopes_and_follow_option
|
|
58
|
+
TmpFile.with_dir do |dir|
|
|
59
|
+
parent = write_chat(dir, 'parent.chat',
|
|
60
|
+
receipt_chat_text(
|
|
61
|
+
{'a1' => [meta_receipt('pt=100 ct=50 tt=150 inference_id=w1'),
|
|
62
|
+
meta_receipt('pt=20 ct=10 tt=30 inference_id=w2')]},
|
|
63
|
+
extra: ['meta: pt=60 ct=40 tt=100 inference_id=p1']
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
aggregate = Chat.provenance_token_totals(parent)
|
|
67
|
+
assert_equal({pt: 180, ct: 100, tt: 280, cct: 0, cwt: 0, rt: 0}, aggregate)
|
|
68
|
+
|
|
69
|
+
# Receipt events are not local, but they are receipt evidence.
|
|
70
|
+
local = Chat.provenance_token_totals(parent, scope: :chat_evidence)
|
|
71
|
+
assert_equal({pt: 60, ct: 40, tt: 100, cct: 0, cwt: 0, rt: 0}, local)
|
|
72
|
+
|
|
73
|
+
receipt = Chat.provenance_token_totals(parent, scope: :receipt_evidence)
|
|
74
|
+
assert_equal({pt: 120, ct: 60, tt: 180, cct: 0, cwt: 0, rt: 0}, receipt)
|
|
75
|
+
|
|
76
|
+
# Totals keep working with a restricted traversal (no jobs to follow).
|
|
77
|
+
assert_equal aggregate, Chat.provenance_token_totals(parent, follow: [:job])
|
|
78
|
+
|
|
79
|
+
# Chat.tokens now uses the collector.
|
|
80
|
+
assert_equal aggregate, Chat.tokens(parent)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
## Fixture B - receipt plus saved worker log
|
|
85
|
+
|
|
86
|
+
def test_receipt_and_log_evidence_merge_into_one_event
|
|
87
|
+
TmpFile.with_dir do |dir|
|
|
88
|
+
worker_log = "user: work\n" +
|
|
89
|
+
"meta: pt=100 ct=50 tt=150 inference_id=w1\n" +
|
|
90
|
+
"meta: pt=20 ct=10 tt=30 inference_id=w2\n" +
|
|
91
|
+
"meta: pt=5 ct=5 tt=10 inference_id=w3\n" +
|
|
92
|
+
"assistant: done\n"
|
|
93
|
+
worker = make_job(dir, 'Worker/ask/Default_w', logs: {'agent.chat' => worker_log})
|
|
94
|
+
|
|
95
|
+
parent = write_chat(dir, 'parent.chat',
|
|
96
|
+
receipt_chat_text(
|
|
97
|
+
{'a1' => [meta_receipt('pt=100 ct=50 tt=150 inference_id=w1'),
|
|
98
|
+
meta_receipt('pt=20 ct=10 tt=30 inference_id=w2')]},
|
|
99
|
+
extra: ['meta: pt=60 ct=40 tt=100 inference_id=p1',
|
|
100
|
+
"meta: job=#{worker}"]
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
events = Chat.provenance_token_events(parent)
|
|
104
|
+
assert_equal 4, events.length
|
|
105
|
+
|
|
106
|
+
merged = events.select { |event| %w[w1 w2].include?(event[:inference_id] )}.sort_by { |event| event[:inference_id] }
|
|
107
|
+
merged.each do |event|
|
|
108
|
+
assert_equal 2, event[:evidence].length
|
|
109
|
+
origins = event[:evidence].collect { |record| record[:origin] }
|
|
110
|
+
assert_include origins, :chat_meta
|
|
111
|
+
assert_include origins, :agent_meta
|
|
112
|
+
|
|
113
|
+
chat_side = event[:evidence].find { |record| record[:origin] == :chat_meta }
|
|
114
|
+
receipt_side = event[:evidence].find { |record| record[:origin] == :agent_meta }
|
|
115
|
+
assert_equal File.join(worker + '.files', 'log', 'agent.chat'), chat_side[:source]
|
|
116
|
+
assert_equal [parent, 2, :agent_meta, event[:inference_id] == 'w1' ? 0 : 1],
|
|
117
|
+
receipt_side[:evidence_address]
|
|
118
|
+
assert !event[:conflict]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Aggregate is fixture A plus the worker-only event, never doubled.
|
|
122
|
+
assert_equal({pt: 185, ct: 105, tt: 290, cct: 0, cwt: 0, rt: 0},
|
|
123
|
+
Chat.provenance_token_totals(parent))
|
|
124
|
+
|
|
125
|
+
# Totals equal the sum over events.
|
|
126
|
+
summed = events.each_with_object(Hash.new(0)) do |event, hash|
|
|
127
|
+
event[:tokens].each { |key, value| hash[key] += value }
|
|
128
|
+
end
|
|
129
|
+
assert_equal Chat.provenance_token_totals(parent), summed
|
|
130
|
+
|
|
131
|
+
assert_equal Chat.provenance_token_totals(parent), Chat.tokens(parent)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_scope_selection_with_shared_evidence
|
|
136
|
+
TmpFile.with_dir do |dir|
|
|
137
|
+
worker_log = "user: work\n" +
|
|
138
|
+
"meta: pt=100 ct=50 tt=150 inference_id=w1\n" +
|
|
139
|
+
"meta: pt=5 ct=5 tt=10 inference_id=w3\n" +
|
|
140
|
+
"assistant: done\n"
|
|
141
|
+
worker = make_job(dir, 'Worker/ask/Default_w', logs: {'agent.chat' => worker_log})
|
|
142
|
+
|
|
143
|
+
parent = write_chat(dir, 'parent.chat',
|
|
144
|
+
receipt_chat_text(
|
|
145
|
+
{'a1' => [meta_receipt('pt=100 ct=50 tt=150 inference_id=w1')]},
|
|
146
|
+
extra: ['meta: pt=60 ct=40 tt=100 inference_id=p1',
|
|
147
|
+
"meta: job=#{worker}"]
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
events = Chat.provenance_token_events(parent)
|
|
151
|
+
shared = events.select { |event| event[:evidence].any? { |r| r[:origin] == :chat_meta } &&
|
|
152
|
+
event[:evidence].any? { |r| r[:origin] == :agent_meta } }
|
|
153
|
+
assert_equal ['w1'], shared.collect { |event| event[:inference_id] }
|
|
154
|
+
|
|
155
|
+
# local and receipt both include the shared event; aggregate counts it once.
|
|
156
|
+
local = Chat.provenance_token_totals(parent, scope: :chat_evidence)
|
|
157
|
+
receipt = Chat.provenance_token_totals(parent, scope: :receipt_evidence)
|
|
158
|
+
aggregate = Chat.provenance_token_totals(parent)
|
|
159
|
+
shared_tokens = shared.first[:tokens]
|
|
160
|
+
|
|
161
|
+
local_events = events.select { |event| event[:evidence].any? { |r| r[:origin] == :chat_meta } }
|
|
162
|
+
receipt_events = events.select { |event| event[:evidence].any? { |r| r[:origin] == :agent_meta } }
|
|
163
|
+
assert_equal local_events.length + receipt_events.length - shared.length, events.length
|
|
164
|
+
|
|
165
|
+
summed = shared_tokens.each_with_object(Hash.new(0)) { |(k, v), h| h[k] = local[k] + receipt[k] - v }
|
|
166
|
+
assert_equal aggregate, summed
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
## Fixture D - aggregate over the nested chain
|
|
171
|
+
|
|
172
|
+
def test_fixture_d_every_direct_event_counted_once
|
|
173
|
+
TmpFile.with_dir do |dir|
|
|
174
|
+
parent, _worker, _critic, _dep = fixture_c(dir)
|
|
175
|
+
|
|
176
|
+
events = Chat.provenance_token_events(parent)
|
|
177
|
+
|
|
178
|
+
# w1, w2 (worker log), c1 (critic receipt inside the worker log),
|
|
179
|
+
# shadow (parent receipt); the two job= projections count for nothing.
|
|
180
|
+
assert_equal %w[c1 shadow w1 w2], events.collect { |event| event[:inference_id] }.sort
|
|
181
|
+
assert_empty events.select { |event| event[:meta][:job] }
|
|
182
|
+
|
|
183
|
+
# By hand: parent receipt shadow (10/0/12) + worker log w1 (100/50/150)
|
|
184
|
+
# + w2 (20/10/30) + critic receipt c1 (30/10/40).
|
|
185
|
+
assert_equal({pt: 160, ct: 70, tt: 232, cct: 0, cwt: 0, rt: 0},
|
|
186
|
+
Chat.provenance_token_totals(parent))
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
## Fixture F - identity conflicts
|
|
191
|
+
|
|
192
|
+
def test_conflicting_token_fields_count_only_canonical_evidence
|
|
193
|
+
TmpFile.with_dir do |dir|
|
|
194
|
+
log = "user: work\nmeta: pt=10 ct=5 tt=99 inference_id=f1\nassistant: done\n"
|
|
195
|
+
worker = make_job(dir, 'Worker/ask/Default_w', logs: {'agent.chat' => log})
|
|
196
|
+
|
|
197
|
+
parent = write_chat(dir, 'parent.chat',
|
|
198
|
+
receipt_chat_text(
|
|
199
|
+
{'a1' => [meta_receipt('pt=10 ct=5 tt=15 inference_id=f1')]},
|
|
200
|
+
extra: ["meta: job=#{worker}"]
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
warnings = []
|
|
204
|
+
events = Chat.provenance_token_events(parent, warnings: warnings)
|
|
205
|
+
|
|
206
|
+
assert_equal 1, events.length
|
|
207
|
+
event = events.first
|
|
208
|
+
assert event[:conflict]
|
|
209
|
+
|
|
210
|
+
# Canonical is the chat side; tokens are neither the sum nor the receipt.
|
|
211
|
+
assert_equal 99, event[:tokens][:tt]
|
|
212
|
+
assert_equal({pt: 10, ct: 5, tt: 99, cct: 0, cwt: 0, rt: 0},
|
|
213
|
+
Chat.provenance_token_totals(parent))
|
|
214
|
+
|
|
215
|
+
# Both evidence records survive, each with its own meta.
|
|
216
|
+
assert_equal 2, event[:evidence].length
|
|
217
|
+
assert_equal 99, event[:evidence].find { |r| r[:origin] == :chat_meta }[:meta][:tt].to_i
|
|
218
|
+
assert_equal 15, event[:evidence].find { |r| r[:origin] == :agent_meta }[:meta][:tt].to_i
|
|
219
|
+
|
|
220
|
+
assert_equal 1, warnings.length
|
|
221
|
+
warning = warnings.first
|
|
222
|
+
assert_equal :identity_conflict, warning[:reason]
|
|
223
|
+
assert_equal 'f1', warning[:inference_id]
|
|
224
|
+
assert_equal [:inference_id, 'f1'], warning[:identity]
|
|
225
|
+
assert_equal [:tt], warning[:fields]
|
|
226
|
+
assert_equal [99, 15], warning[:values][:tt].collect(&:to_i)
|
|
227
|
+
assert_equal 2, warning[:evidence].length
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def test_provider_response_id_disagreement_is_a_conflict
|
|
232
|
+
TmpFile.with_dir do |dir|
|
|
233
|
+
parent = write_chat(dir, 'p2.chat',
|
|
234
|
+
receipt_chat_text(
|
|
235
|
+
{'a1' => [meta_receipt('pt=1 tt=2 inference_id=g1 provider_response_id=ra'),
|
|
236
|
+
meta_receipt('pt=1 tt=2 inference_id=g1 provider_response_id=rb')]}
|
|
237
|
+
))
|
|
238
|
+
|
|
239
|
+
warnings = []
|
|
240
|
+
events = Chat.provenance_token_events(parent, warnings: warnings)
|
|
241
|
+
|
|
242
|
+
assert_equal 1, events.length
|
|
243
|
+
assert events.first[:conflict]
|
|
244
|
+
assert_equal 2, events.first[:evidence].length
|
|
245
|
+
assert_equal 1, warnings.length
|
|
246
|
+
assert_equal [:provider_response_id], warnings.first[:fields]
|
|
247
|
+
assert_equal %w[ra rb], warnings.first[:values][:provider_response_id]
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_provider_response_id_groups_receipt_and_log_without_inference_id
|
|
252
|
+
TmpFile.with_dir do |dir|
|
|
253
|
+
worker = make_job(dir, 'W3/ask/Default_3',
|
|
254
|
+
logs: {'agent.chat' => "user: w\nmeta: pt=1 tt=2 provider_response_id=rx\nassistant: done\n"})
|
|
255
|
+
parent = write_chat(dir, 'p4.chat',
|
|
256
|
+
receipt_chat_text(
|
|
257
|
+
{'a1' => [meta_receipt('pt=1 tt=2 provider_response_id=rx')]},
|
|
258
|
+
extra: ["meta: job=#{worker}"]
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
events = Chat.provenance_token_events(parent)
|
|
262
|
+
|
|
263
|
+
assert_equal 1, events.length
|
|
264
|
+
assert_equal :provider_response_id, events.first[:deduplication]
|
|
265
|
+
assert_equal [:provider_response_id, 'rx'], events.first[:identity]
|
|
266
|
+
assert_equal 2, events.first[:evidence].length
|
|
267
|
+
assert_equal({pt: 1, ct: 0, tt: 2, cct: 0, cwt: 0, rt: 0},
|
|
268
|
+
Chat.provenance_token_totals(parent))
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_conflicts_do_not_raise_without_a_warnings_array
|
|
273
|
+
TmpFile.with_dir do |dir|
|
|
274
|
+
parent = write_chat(dir, 'p2.chat',
|
|
275
|
+
receipt_chat_text(
|
|
276
|
+
{'a1' => [meta_receipt('pt=1 tt=2 inference_id=g1'),
|
|
277
|
+
meta_receipt('pt=1 tt=9 inference_id=g1')]}
|
|
278
|
+
))
|
|
279
|
+
|
|
280
|
+
events = Chat.provenance_token_events(parent)
|
|
281
|
+
assert_equal 1, events.length
|
|
282
|
+
assert events.first[:conflict]
|
|
283
|
+
assert_equal 2, events.first[:evidence].length
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
## Fixture G - legacy records without inference_id
|
|
288
|
+
|
|
289
|
+
def test_legacy_chat_metas_keep_lineage_dedup
|
|
290
|
+
TmpFile.with_dir do |dir|
|
|
291
|
+
legacy = "user: w\nmeta: pt=7 ct=3 tt=10\nassistant: done\n"
|
|
292
|
+
j1 = make_job(dir, 'J1/ask/Default_1', logs: {'agent.chat' => legacy})
|
|
293
|
+
j2 = make_job(dir, 'J2/ask/Default_2', logs: {'agent.chat' => legacy})
|
|
294
|
+
copied = write_chat(dir, 'copied.chat',
|
|
295
|
+
"user: go\nmeta: job=#{j1}\nmeta: job=#{j2}\nassistant: done\n")
|
|
296
|
+
|
|
297
|
+
events = Chat.provenance_token_events(copied)
|
|
298
|
+
|
|
299
|
+
assert_equal 1, events.length
|
|
300
|
+
assert_equal :legacy_lineage, events.first[:deduplication]
|
|
301
|
+
assert_equal [:lineage, events.first[:identity][1]], events.first[:identity]
|
|
302
|
+
assert_equal({pt: 7, ct: 3, tt: 10, cct: 0, cwt: 0, rt: 0},
|
|
303
|
+
Chat.provenance_token_totals(copied))
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def test_legacy_receipt_metas_are_never_merged
|
|
308
|
+
TmpFile.with_dir do |dir|
|
|
309
|
+
twice = write_chat(dir, 'twice.chat',
|
|
310
|
+
receipt_chat_text(
|
|
311
|
+
{'r1' => [meta_receipt('pt=5 tt=6')],
|
|
312
|
+
'r2' => [meta_receipt('pt=5 tt=6')]}
|
|
313
|
+
))
|
|
314
|
+
|
|
315
|
+
events = Chat.provenance_token_events(twice)
|
|
316
|
+
|
|
317
|
+
# No exact rule exists for legacy receipts: identical content stays two
|
|
318
|
+
# events (documented possible overcount).
|
|
319
|
+
assert_equal 2, events.length
|
|
320
|
+
assert events.all? { |event| event[:deduplication] == :receipt_unresolved }
|
|
321
|
+
assert_equal 2, events.collect { |event| event[:identity] }.uniq.length
|
|
322
|
+
assert_equal({pt: 10, ct: 0, tt: 12, cct: 0, cwt: 0, rt: 0},
|
|
323
|
+
Chat.provenance_token_totals(twice))
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def test_legacy_receipt_and_log_stay_two_events
|
|
328
|
+
TmpFile.with_dir do |dir|
|
|
329
|
+
worker = make_job(dir, 'LG/ask/Default_1',
|
|
330
|
+
logs: {'agent.chat' => "user: w\nmeta: pt=8 tt=9\nassistant: done\n"})
|
|
331
|
+
mixed = write_chat(dir, 'mixed.chat',
|
|
332
|
+
receipt_chat_text(
|
|
333
|
+
{'r1' => [meta_receipt('pt=8 tt=9')]},
|
|
334
|
+
extra: ["meta: job=#{worker}"]
|
|
335
|
+
))
|
|
336
|
+
|
|
337
|
+
events = Chat.provenance_token_events(mixed)
|
|
338
|
+
|
|
339
|
+
assert_equal 2, events.length
|
|
340
|
+
assert_equal 1, events.count { |event| event[:deduplication] == :legacy_lineage }
|
|
341
|
+
assert_equal 1, events.count { |event| event[:deduplication] == :receipt_unresolved }
|
|
342
|
+
# Documented overcount for legacy data.
|
|
343
|
+
assert_equal({pt: 16, ct: 0, tt: 18, cct: 0, cwt: 0, rt: 0},
|
|
344
|
+
Chat.provenance_token_totals(mixed))
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_chat_without_agent_meta_keeps_token_totals_semantics
|
|
349
|
+
TmpFile.with_dir do |dir|
|
|
350
|
+
path = write_chat(dir, 'plain.chat',
|
|
351
|
+
"user: hi\nmeta: pt=1 tt=2\nmeta: pt=3 tt=4\nassistant: done\n")
|
|
352
|
+
|
|
353
|
+
assert_equal Chat.token_totals([Chat.load(path)]),
|
|
354
|
+
Chat.provenance_token_totals(path)
|
|
355
|
+
assert_equal Chat.token_totals([Chat.load(path)]), Chat.tokens(path)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
## Fixture H - truncated parent output
|
|
360
|
+
|
|
361
|
+
def test_truncated_output_keeps_receipt_events
|
|
362
|
+
TmpFile.with_dir do |dir|
|
|
363
|
+
parent = write_chat(dir, 'parent.chat',
|
|
364
|
+
truncated_receipt_chat(
|
|
365
|
+
'a1', [meta_receipt('pt=200 ct=100 tt=300 inference_id=t1')]
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
warnings = []
|
|
369
|
+
events = Chat.provenance_token_events(parent, warnings: warnings)
|
|
370
|
+
|
|
371
|
+
assert_equal ['t1'], events.collect { |event| event[:inference_id] }
|
|
372
|
+
assert_equal({pt: 200, ct: 100, tt: 300, cct: 0, cwt: 0, rt: 0},
|
|
373
|
+
Chat.provenance_token_totals(parent))
|
|
374
|
+
|
|
375
|
+
# Truncation is a transport fact, not a token problem.
|
|
376
|
+
assert_empty warnings
|
|
377
|
+
status = Chat.tool_call_status(Chat.tool_calls(Chat.load(parent)).first)
|
|
378
|
+
assert_equal false, status[:success]
|
|
379
|
+
assert_match(/90,?000 characters/, status[:exception])
|
|
380
|
+
|
|
381
|
+
totals = Chat.provenance_token_totals(parent)
|
|
382
|
+
assert_equal 0, totals.values.select { |v| v > 300 }.length
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
## Canonical rule
|
|
387
|
+
|
|
388
|
+
def test_canonical_evidence_prefers_chat_side_then_discovery_order
|
|
389
|
+
TmpFile.with_dir do |dir|
|
|
390
|
+
worker = make_job(dir, 'W/ask/Default_w',
|
|
391
|
+
logs: {'agent.chat' => "user: w\nmeta: pt=11 ct=7 tt=18 inference_id=k1\nassistant: done\n"})
|
|
392
|
+
parent = write_chat(dir, 'parent.chat',
|
|
393
|
+
receipt_chat_text(
|
|
394
|
+
{'a1' => [meta_receipt('pt=22 ct=14 tt=36 inference_id=k1')]},
|
|
395
|
+
extra: ["meta: job=#{worker}"]
|
|
396
|
+
))
|
|
397
|
+
|
|
398
|
+
event = Chat.provenance_token_events(parent).first
|
|
399
|
+
|
|
400
|
+
# Chat side wins over the receipt.
|
|
401
|
+
assert_equal :chat_meta, event[:evidence].first[:origin]
|
|
402
|
+
assert_equal 11, event[:meta][:pt].to_i
|
|
403
|
+
assert_equal({pt: 11, ct: 7, tt: 18, cct: 0, cwt: 0, rt: 0}, event[:tokens])
|
|
404
|
+
|
|
405
|
+
# Within one origin, discovery order decides (first receipt of two).
|
|
406
|
+
other = write_chat(dir, 'two.chat',
|
|
407
|
+
receipt_chat_text(
|
|
408
|
+
{'a1' => [meta_receipt('pt=1 tt=2 inference_id=d1'),
|
|
409
|
+
meta_receipt('pt=1 tt=2 inference_id=d1')]}
|
|
410
|
+
))
|
|
411
|
+
assert_equal 1, Chat.provenance_token_events(other).length
|
|
412
|
+
assert_equal 1, Chat.provenance_token_totals(other)[:pt]
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
## Traversal-stage receipt problems vs the warnings Array
|
|
417
|
+
|
|
418
|
+
def test_traversal_stage_receipt_problems_go_to_the_warnings_array
|
|
419
|
+
TmpFile.with_dir do |dir|
|
|
420
|
+
missing = File.join(dir, 'Missing/ask/Default_zzz')
|
|
421
|
+
chat = write_chat(dir, 'bad.chat',
|
|
422
|
+
receipt_chat_text(
|
|
423
|
+
{'b1' => 'not-an-array',
|
|
424
|
+
'b2' => [meta_receipt("job=#{missing}")]},
|
|
425
|
+
extra: ['meta: pt=5 tt=6 inference_id=ok1']
|
|
426
|
+
))
|
|
427
|
+
|
|
428
|
+
warnings = []
|
|
429
|
+
events = Chat.provenance_token_events(chat, warnings: warnings)
|
|
430
|
+
|
|
431
|
+
# The good direct event still comes through; nothing raised.
|
|
432
|
+
assert_equal ['ok1'], events.collect { |event| event[:inference_id] }
|
|
433
|
+
|
|
434
|
+
# Totals use their own buffer (each API call reports its own warnings).
|
|
435
|
+
totals_warnings = []
|
|
436
|
+
assert_equal({pt: 5, ct: 0, tt: 6, cct: 0, cwt: 0, rt: 0},
|
|
437
|
+
Chat.provenance_token_totals(chat, warnings: totals_warnings))
|
|
438
|
+
assert_equal warnings.collect { |w| w[:reason] }, totals_warnings.collect { |w| w[:reason] }
|
|
439
|
+
|
|
440
|
+
# One warning per problem, in the agent_meta_error_reference shape plus
|
|
441
|
+
# the error message; the same problem is not reported twice.
|
|
442
|
+
reasons = warnings.collect { |warning| warning[:reason] }
|
|
443
|
+
assert_equal %i[not_an_array unresolved_job_reference], reasons
|
|
444
|
+
not_an_array = warnings.first
|
|
445
|
+
assert_equal chat, not_an_array[:source]
|
|
446
|
+
assert_equal [chat, 2], not_an_array[:output_address]
|
|
447
|
+
assert_equal 'b1', not_an_array[:call_id]
|
|
448
|
+
assert_equal 'ask', not_an_array[:tool_name]
|
|
449
|
+
assert_nil not_an_array[:evidence_address]
|
|
450
|
+
assert_match(/not_an_array/, not_an_array[:message])
|
|
451
|
+
|
|
452
|
+
unresolved = warnings.last
|
|
453
|
+
assert_equal missing, unresolved[:reference]
|
|
454
|
+
assert_equal [chat, 4, :agent_meta, 0], unresolved[:evidence_address]
|
|
455
|
+
assert_equal 'b2', unresolved[:call_id]
|
|
456
|
+
assert_match(/unresolved_job_reference/, unresolved[:message])
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def test_traversal_stage_receipt_problems_raise_without_warnings_array
|
|
461
|
+
TmpFile.with_dir do |dir|
|
|
462
|
+
chat = write_chat(dir, 'bad.chat',
|
|
463
|
+
receipt_chat_text({'b1' => 'not-an-array'},
|
|
464
|
+
extra: ['meta: pt=5 tt=6 inference_id=ok1']))
|
|
465
|
+
|
|
466
|
+
error = assert_raise(ScoutException) do
|
|
467
|
+
Chat.provenance_token_events(chat)
|
|
468
|
+
end
|
|
469
|
+
assert_match(/not_an_array/, error.message)
|
|
470
|
+
assert_match(/#{Regexp.escape(chat)}/, error.message)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
## Reviewer cases: multi-location evidence, shared-job receipts, strict mode
|
|
475
|
+
|
|
476
|
+
# The same inference_id persisted in three ordinary chat/log locations plus
|
|
477
|
+
# one receipt must produce ONE event whose evidence array keeps all four
|
|
478
|
+
# addresses, counted once.
|
|
479
|
+
def test_same_identity_in_three_chat_locations_and_one_receipt
|
|
480
|
+
TmpFile.with_dir do |dir|
|
|
481
|
+
shared = 'meta: pt=100 ct=40 tt=140 inference_id=shared1'
|
|
482
|
+
j1 = make_job(dir, 'J1/ask/Default_1', logs: {'agent.chat' => "user: w\n#{shared}\nassistant: done\n"})
|
|
483
|
+
j2 = make_job(dir, 'J2/ask/Default_2', logs: {'agent.chat' => "user: w\n#{shared}\nassistant: done\n"})
|
|
484
|
+
parent = write_chat(dir, 'parent.chat',
|
|
485
|
+
receipt_chat_text(
|
|
486
|
+
{'a1' => [meta_receipt('pt=100 ct=40 tt=140 inference_id=shared1')]},
|
|
487
|
+
extra: [shared, "meta: job=#{j1}", "meta: job=#{j2}"]
|
|
488
|
+
))
|
|
489
|
+
|
|
490
|
+
events = Chat.provenance_token_events(parent).select { |event| event[:inference_id] == 'shared1' }
|
|
491
|
+
|
|
492
|
+
assert_equal 1, events.length
|
|
493
|
+
event = events.first
|
|
494
|
+
assert_equal 4, event[:evidence].length
|
|
495
|
+
|
|
496
|
+
chat_side = event[:evidence].select { |record| record[:origin] == :chat_meta }
|
|
497
|
+
assert_equal 3, chat_side.length
|
|
498
|
+
expected_sources = [parent,
|
|
499
|
+
File.join(j1 + '.files', 'log', 'agent.chat'),
|
|
500
|
+
File.join(j2 + '.files', 'log', 'agent.chat')].collect(&:to_s).sort
|
|
501
|
+
assert_equal expected_sources, chat_side.collect { |record| record[:source].to_s }.sort
|
|
502
|
+
|
|
503
|
+
receipt_side = event[:evidence].select { |record| record[:origin] == :agent_meta }
|
|
504
|
+
assert_equal 1, receipt_side.length
|
|
505
|
+
assert_equal 'a1', receipt_side.first[:call_id]
|
|
506
|
+
assert !event[:conflict]
|
|
507
|
+
|
|
508
|
+
# Counted exactly once.
|
|
509
|
+
assert_equal({pt: 100, ct: 40, tt: 140, cct: 0, cwt: 0, rt: 0},
|
|
510
|
+
Chat.provenance_token_totals(parent))
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Two separate ask receipts pointing at the same job= keep both edge details
|
|
515
|
+
# (distinct call ids and receipt addresses) while the Step is visited once.
|
|
516
|
+
def test_two_receipts_to_the_same_job_keep_both_edge_details
|
|
517
|
+
TmpFile.with_dir do |dir|
|
|
518
|
+
worker = make_job(dir, 'Worker/ask/Default_w',
|
|
519
|
+
logs: {'agent.chat' => "user: w\nmeta: pt=1 tt=2 inference_id=wonly\nassistant: done\n"})
|
|
520
|
+
parent = write_chat(dir, 'parent.chat',
|
|
521
|
+
receipt_chat_text(
|
|
522
|
+
{'a1' => [meta_receipt("job=#{worker}")],
|
|
523
|
+
'a2' => [meta_receipt("job=#{worker}")]}
|
|
524
|
+
))
|
|
525
|
+
|
|
526
|
+
edges = Chat.provenance_edges(parent).select { |edge| edge[:relation] == :agent_job }
|
|
527
|
+
assert_equal 2, edges.length
|
|
528
|
+
details = edges.collect { |edge| edge[:detail] }
|
|
529
|
+
assert_equal %w[a1 a2], details.collect { |detail| detail[:call_id] }.sort
|
|
530
|
+
assert_equal 2, details.collect { |detail| detail[:evidence_address] }.uniq.length
|
|
531
|
+
assert(edges.all? { |edge| edge[:to].path.to_s == worker.to_s })
|
|
532
|
+
assert(edges.all? { |edge| edge[:from].to_s == parent.to_s })
|
|
533
|
+
|
|
534
|
+
# One unique Step node; each edge visits it, only the first expands it.
|
|
535
|
+
assert_equal 1, Chat.provenance_jobs(parent).length
|
|
536
|
+
job_visits = Chat.traverse_provenance(parent).to_a
|
|
537
|
+
.select { |kind, object, *_rest| kind == :job && object.path.to_s == worker.to_s }
|
|
538
|
+
assert_equal 2, job_visits.length
|
|
539
|
+
assert_equal 1, job_visits.count { |visit| visit[5] }
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def test_strict_mode_raises_on_identity_conflict
|
|
544
|
+
TmpFile.with_dir do |dir|
|
|
545
|
+
parent = write_chat(dir, 'p5.chat',
|
|
546
|
+
receipt_chat_text(
|
|
547
|
+
{'a1' => [meta_receipt('pt=1 tt=2 inference_id=g1'),
|
|
548
|
+
meta_receipt('pt=1 tt=9 inference_id=g1')]}
|
|
549
|
+
))
|
|
550
|
+
|
|
551
|
+
assert_raise(ScoutException) do
|
|
552
|
+
Chat.provenance_token_events(parent, strict: true)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# A provider_response_id present in one copy and absent in another is
|
|
558
|
+
# incomplete evidence (migration), never a conflict.
|
|
559
|
+
def test_missing_provider_response_id_is_incomplete_not_conflict
|
|
560
|
+
TmpFile.with_dir do |dir|
|
|
561
|
+
worker = make_job(dir, 'W4/ask/Default_4',
|
|
562
|
+
logs: {'agent.chat' => "user: w\nmeta: pt=3 tt=4 inference_id=h1 provider_response_id=rq\nassistant: done\n"})
|
|
563
|
+
parent = write_chat(dir, 'p6.chat',
|
|
564
|
+
receipt_chat_text(
|
|
565
|
+
{'a1' => [meta_receipt('pt=3 tt=4 inference_id=h1')]},
|
|
566
|
+
extra: ["meta: job=#{worker}"]
|
|
567
|
+
))
|
|
568
|
+
|
|
569
|
+
warnings = []
|
|
570
|
+
events = Chat.provenance_token_events(parent, warnings: warnings)
|
|
571
|
+
|
|
572
|
+
event = events.find { |item| item[:inference_id] == 'h1' }
|
|
573
|
+
assert event
|
|
574
|
+
assert_equal 2, event[:evidence].length
|
|
575
|
+
assert event[:incomplete_evidence]
|
|
576
|
+
assert !event[:conflict]
|
|
577
|
+
assert_empty warnings.select { |warning| warning[:reason] == :identity_conflict }
|
|
578
|
+
assert_equal({pt: 3, ct: 0, tt: 4, cct: 0, cwt: 0, rt: 0},
|
|
579
|
+
Chat.provenance_token_totals(parent))
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
## Scope validation
|
|
584
|
+
|
|
585
|
+
def test_unknown_scope_raises
|
|
586
|
+
TmpFile.with_dir do |dir|
|
|
587
|
+
path = write_chat(dir, 'any.chat', "user: hi\nassistant: done\n")
|
|
588
|
+
|
|
589
|
+
assert_raise(ParameterException) do
|
|
590
|
+
Chat.provenance_token_totals(path, scope: :bogus)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
end
|