Files
willtools-demo/WillTools.extension/tests/test_aiextract_run.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
2026-09-09 08:32:15 -05:00

1328 lines
54 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for tools/aiextract_run.py -- the DSV4F-vision half of Design Extract.
Loaded by path, like `test_knowledgelint.py` and `test_scrub.py`: the tool
is host CPython (it needs a real HTTP client and, eventually, a real dsh)
and lives outside `WillTools.extension` entirely.
What is tested here is the part that makes an unsupervised, detached process
safe to run at all: `apply_ai_fill` (never overwrite a value the mechanical
reader placed; only fill what was empty, or add distinct new lines to the
two collections of independent entries) and `apply_failure_note` (a run that
cannot complete leaves every field exactly as it found them and says why in
one line). The dsh round trip is exercised with the HTTP boundary faked --
`transport` is a plain callable substituted for `urllib.request.urlopen`,
per `_rpc`'s own docstring -- never a real socket. The dsh wire format
itself (payload shapes, whether `session.history` really carries a status
field) is UNVERIFIED against a live dsh; see aiextract_run.py's module
docstring. That gap, and the Revit-side image export in the button, are on
the owner-must-verify list, not here.
"""
import io
import json
import os
import sys
import pytest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
TOOLS = os.path.join(ROOT, 'tools')
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
aiextract_run = pytest.importorskip('aiextract_run')
from vibe import specformat
# --------------------------------------------------------------- fixtures
def _blank_spec(design='PNL1'):
"""A spec shaped the way `designspec.build_spec` actually leaves one --
schema set, design number set, source.extractor stamped 'regex' -- so a
test asserting the AI pass left provenance alone has a real provenance
to compare against instead of the raw template's all-None source block.
"""
spec = json.loads(json.dumps(specformat.SPEC_TEMPLATE))
spec['design_number'] = design
spec['schema'] = specformat.SCHEMA_VERSION
spec['source']['extractor'] = 'regex'
return spec
def _write_spec(path, spec):
with io.open(path, 'w', encoding='utf-8') as fh:
fh.write(specformat.spec_text(spec))
def _read_spec(path):
with io.open(path, 'r', encoding='utf-8') as fh:
return specformat.normalize_spec(specformat.parse_spec_text(fh.read()))
# ------------------------------------------------------------ apply_ai_fill
def test_never_overwrites_a_scalar_the_regex_reader_already_placed():
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
ai = {'concrete': {'fc_psi': 9999, 'fci_psi': 3500}}
merged, filled = aiextract_run.apply_ai_fill(spec, ai)
assert merged['concrete']['fc_psi'] == 6000 # untouched
assert merged['concrete']['fci_psi'] == 3500 # filled
assert filled == ['concrete.fci_psi']
def test_fills_a_blank_scalar():
spec = _blank_spec()
ai = {'thickness_in': 8.0}
merged, filled = aiextract_run.apply_ai_fill(spec, ai)
assert merged['thickness_in'] == 8.0
assert filled == ['thickness_in']
def test_does_not_fill_when_ai_proposes_nothing():
spec = _blank_spec()
merged, filled = aiextract_run.apply_ai_fill(spec, {})
assert merged == spec
assert filled == []
def test_structured_list_filled_only_when_currently_empty():
spec = _blank_spec()
ai = {'reinforcing': {'strand_layout': {
'columns_in': [3.5, 16.5, 40.5],
'rows': [{'from_bottom_in': 1.75, 'strands': [True, True, True]}],
'total_in': 44.0,
}}}
merged, filled = aiextract_run.apply_ai_fill(spec, ai)
layout = merged['reinforcing']['strand_layout']
assert layout['columns_in'] == [3.5, 16.5, 40.5]
assert layout['total_in'] == 44.0
assert 'reinforcing.strand_layout.columns_in' in filled
assert 'reinforcing.strand_layout.rows' in filled
assert 'reinforcing.strand_layout.total_in' in filled
def test_structured_list_never_touched_once_non_empty():
spec = _blank_spec()
placed = [{'qty': 14, 'dia_in': 0.5, 'grade_ksi': 270, 'it_lb': 31600,
'role': 'main', 'note': None}]
spec['reinforcing']['strand_groups'] = placed
ai = {'reinforcing': {'strand_groups': [
{'qty': 99, 'dia_in': 0.6, 'grade_ksi': 300, 'it_lb': 1,
'role': 'bogus', 'note': None}]}}
merged, filled = aiextract_run.apply_ai_fill(spec, ai)
assert merged['reinforcing']['strand_groups'] == placed
assert filled == []
def test_datum_is_never_a_content_path_at_all():
# The datum is plant knowledge, never something the model may set --
# apply_ai_fill must not even have a rule for it under any name.
assert not any('datum' in p for p in aiextract_run.CONTENT_PATHS)
assert not any('datum' in p for p in aiextract_run.STRUCTURED_LIST_PATHS)
def test_atomic_lists_append_new_distinct_entries_and_keep_old_ones():
spec = _blank_spec()
spec['unresolved'] = [{'text': 'STRIPPING DETAIL < 25\'', 'reason': 'prose'}]
ai = {'unresolved': [
{'text': 'STRIPPING DETAIL < 25\'', 'reason': 'prose'}, # duplicate
{'text': 'chain total disagrees with piece width by 1/4"',
'reason': 'flagged by vision pass'},
]}
merged, filled = aiextract_run.apply_ai_fill(spec, ai)
assert len(merged['unresolved']) == 2
assert merged['unresolved'][0]['text'] == 'STRIPPING DETAIL < 25\''
assert merged['unresolved'][1]['text'].startswith('chain total')
assert 'unresolved' in filled
def test_atomic_list_merge_is_case_and_whitespace_tolerant_for_dedupe():
spec = _blank_spec()
spec['notes'] = ['USE MC STANDARD REINFORCEMENT FOR CUT DOWN PANELS']
ai = {'notes': ['use mc standard reinforcement for cut down panels']}
merged, filled = aiextract_run.apply_ai_fill(spec, ai)
assert len(merged['notes']) == 1
assert filled == []
def test_confidence_accepted_only_for_paths_actually_just_filled():
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
ai = {
'concrete': {'fci_psi': 3500},
# This one names an ALREADY-FILLED field, so it must be dropped even
# though the model is "confident" about it -- it did not just fill it.
'confidence': {'concrete.fci_psi': 'high', 'concrete.fc_psi': 'high'},
}
merged, _filled = aiextract_run.apply_ai_fill(spec, ai)
assert merged['confidence'] == {'concrete.fci_psi': 'high'}
def test_apply_ai_fill_never_mutates_its_input():
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
before = json.loads(json.dumps(spec))
aiextract_run.apply_ai_fill(spec, {'concrete': {'fci_psi': 3500}})
assert spec == before
# --------------------------------------------------------- apply_failure_note
def test_failure_note_leaves_every_field_value_untouched():
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
spec['unresolved'] = [{'text': 'foo', 'reason': 'bar'}]
result = aiextract_run.apply_failure_note(spec, 'DshTimeout: no reply within 180s')
assert result['concrete']['fc_psi'] == 6000
assert result['source'] == spec['source'] # untouched, no stamp
assert result['unresolved'][0] == {'text': 'foo', 'reason': 'bar'}
assert len(result['unresolved']) == 2
assert result['unresolved'][1]['text'] == aiextract_run.FAILURE_REASON_PREFIX
assert 'DshTimeout' in result['unresolved'][1]['reason']
def test_failure_note_does_not_pile_up_on_repeated_identical_failures():
spec = _blank_spec()
once = aiextract_run.apply_failure_note(spec, 'same reason')
twice = aiextract_run.apply_failure_note(once, 'same reason')
assert len(twice['unresolved']) == 1
# ------------------------------------------------------- extract_json_payload
def test_extract_json_payload_reads_a_fenced_block():
reply = ('Here is what I found.\n\n'
'```json\n{"concrete": {"fci_psi": 3500}}\n```\n\n'
'Summary: filled fci_psi.')
payload = aiextract_run.extract_json_payload(reply)
assert payload == {'concrete': {'fci_psi': 3500}}
def test_extract_json_payload_falls_back_to_first_balanced_object():
reply = 'no fence here, just {"thickness_in": 8.0} inline.'
assert aiextract_run.extract_json_payload(reply) == {'thickness_in': 8.0}
def test_extract_json_payload_raises_rather_than_returning_empty():
with pytest.raises(ValueError):
aiextract_run.extract_json_payload('I could not find anything.')
def test_extract_json_payload_rejects_a_non_object_json_value():
with pytest.raises(ValueError):
aiextract_run.extract_json_payload('```json\n[1, 2, 3]\n```')
# ----------------------------------------------------------------- prompt
def test_build_prompt_includes_datum_and_evidence_and_template():
spec = _blank_spec()
prompt = aiextract_run.build_prompt(
evidence={'design_number': 'PNL1'}, regex_spec=spec,
datum_clause='the LEFT rail (datum = "left_rail").',
image_paths=[])
assert 'the LEFT rail' in prompt
assert '"design_number": "PNL1"' in prompt
assert 'strand_layout' in prompt
assert 'no drawing image was available' in prompt.lower()
def test_build_prompt_lists_image_paths_when_given():
spec = _blank_spec()
prompt = aiextract_run.build_prompt(
evidence={}, regex_spec=spec, datum_clause='d',
image_paths=['C:\\job\\PNL1.summary.png'])
assert 'C:\\job\\PNL1.summary.png' in prompt
assert 'read_image' in prompt
def test_build_prompt_strand_chain_rules_cover_concatenation_and_rows():
# rule 4a's "How to read the chain" sub-list, added after the real 10"
# slab run that gave up on one 12-segment chain against a stated 19
# strands -- the drawing actually carried two rows, each with its own
# chain, and the bottom chain arrived as four separate dimension
# elements that needed concatenating. Each assertion below is a short,
# distinctive phrase so a wording tweak elsewhere in the rule does not
# break the test.
spec = _blank_spec()
prompt = aiextract_run.build_prompt(
evidence={}, regex_spec=spec, datum_clause='d', image_paths=[])
# (a) same-y dimensions concatenate into one chain.
assert 'concatenate them left to right' in prompt
# (b) one chain per row; a full grid shares a single chain.
assert 'One chain per row' in prompt
assert 'single chain shared by every row' in prompt
# (c) a chain whose total matches neither the width nor a row is ignored.
assert 'NOT a strand chain' in prompt
# count check against the stated quantity (and symbols, when present).
assert 'MUST equal the stated quantity' in prompt
# the STRAND PATTERN (matrix) unresolved-reason pointer.
assert 'STRAND PATTERN (matrix)' in prompt
def _leaf_paths(node, prefix=''):
paths = []
for key, value in node.items():
path = '{0}.{1}'.format(prefix, key) if prefix else key
if isinstance(value, dict) and value:
paths.extend(_leaf_paths(value, path))
else:
paths.append(path)
return paths
def test_reply_template_matches_the_merge_rule_tables_exactly():
# The old opus-CLI prompt drifted from the spec's real field names
# (`spacings_in` / `positions_in` / `strands_per_row`, which stopped
# existing when the matrix shape landed) because nothing exercised it.
# This pins AI_REPLY_TEMPLATE -- what the prompt actually asks the model
# to send back -- against the three tables apply_ai_fill actually reads.
leaves = set(_leaf_paths(aiextract_run.AI_REPLY_TEMPLATE))
expected = (set(aiextract_run.CONTENT_PATHS) |
set(aiextract_run.ATOMIC_LIST_PATHS) | {'confidence'})
assert leaves == expected
# -------------------------------------------------------------- resolve_model
#
# `resolve_model()` now returns `(model, provider, why)` -- see the module
# docstring's point 4: `session.selectModel` needs an explicit provider id
# alongside the model id, and this file was rewritten to fail closed on
# either rather than guess. Every test here stubs `_find_provider_for_model_id`
# unless it is specifically testing provider resolution, so the model-only
# assertions above stay isolated from the real ~/.dsh/settings.yaml this
# machine happens to have -- the same isolation `_scan_settings_for_vision_model`
# is already stubbed for below.
def test_resolve_model_prefers_an_explicit_job_setting(monkeypatch):
monkeypatch.delenv(aiextract_run.MODEL_ENV, raising=False)
monkeypatch.delenv(aiextract_run.PROVIDER_ENV, raising=False)
monkeypatch.setattr(aiextract_run, '_find_provider_for_model_id',
lambda *_a, **_k: ('stub-provider', None))
model, provider, why = aiextract_run.resolve_model(
{'model': 'named-explicitly'})
assert model == 'named-explicitly'
assert provider == 'stub-provider'
assert why is None
def test_resolve_model_falls_back_to_the_env_var(monkeypatch):
monkeypatch.setenv(aiextract_run.MODEL_ENV, 'from-env')
monkeypatch.delenv(aiextract_run.PROVIDER_ENV, raising=False)
monkeypatch.setattr(aiextract_run, '_find_provider_for_model_id',
lambda *_a, **_k: ('stub-provider', None))
model, provider, why = aiextract_run.resolve_model({})
assert model == 'from-env'
assert provider == 'stub-provider'
assert why is None
def test_resolve_model_uses_settings_scan_result(monkeypatch):
monkeypatch.delenv(aiextract_run.MODEL_ENV, raising=False)
monkeypatch.delenv(aiextract_run.PROVIDER_ENV, raising=False)
monkeypatch.setattr(aiextract_run, '_scan_settings_for_vision_model',
lambda: ('commandcode-vision', 'commandcode', None))
model, provider, why = aiextract_run.resolve_model({})
assert model == 'commandcode-vision'
# The scan already knew which provider hosts it -- free, no separate
# `_find_provider_for_model_id` lookup needed.
assert provider == 'commandcode'
assert why is None
def test_resolve_model_falls_back_to_unconfirmed_constant_and_says_so(monkeypatch):
monkeypatch.delenv(aiextract_run.MODEL_ENV, raising=False)
monkeypatch.delenv(aiextract_run.PROVIDER_ENV, raising=False)
monkeypatch.setattr(aiextract_run, '_scan_settings_for_vision_model',
lambda: (None, None, 'no settings file'))
monkeypatch.setattr(aiextract_run, '_find_provider_for_model_id',
lambda *_a, **_k: (None, 'no settings file'))
model, provider, why = aiextract_run.resolve_model({})
assert model == aiextract_run.FALLBACK_VISION_MODEL
assert provider is None
assert why is not None and 'unconfirmed' in why
# --------------------------------------------------- resolve_model: provider
def test_resolve_model_prefers_an_explicit_job_provider(monkeypatch):
monkeypatch.delenv(aiextract_run.PROVIDER_ENV, raising=False)
def _boom(*_a, **_k):
raise AssertionError('should not need to look the provider up')
monkeypatch.setattr(aiextract_run, '_find_provider_for_model_id', _boom)
model, provider, why = aiextract_run.resolve_model(
{'model': 'named-explicitly', 'provider': 'named-provider'})
assert model == 'named-explicitly'
assert provider == 'named-provider'
assert why is None
def test_resolve_model_falls_back_to_the_provider_env_var(monkeypatch):
monkeypatch.setenv(aiextract_run.PROVIDER_ENV, 'env-provider')
def _boom(*_a, **_k):
raise AssertionError('should not need to look the provider up')
monkeypatch.setattr(aiextract_run, '_find_provider_for_model_id', _boom)
model, provider, why = aiextract_run.resolve_model(
{'model': 'named-explicitly'})
assert provider == 'env-provider'
assert why is None
def test_resolve_model_refuses_to_guess_a_provider_it_cannot_find(monkeypatch):
# THE FAIL-CLOSED CASE: a model id came from somewhere, but nothing
# (job, env, settings.yaml) can say which provider serves it --
# `session.selectModel` requires one, so this must never invent it.
monkeypatch.delenv(aiextract_run.PROVIDER_ENV, raising=False)
monkeypatch.setattr(aiextract_run, '_find_provider_for_model_id',
lambda *_a, **_k: (None, 'no entry for this model'))
model, provider, why = aiextract_run.resolve_model(
{'model': 'named-explicitly'})
assert model == 'named-explicitly'
assert provider is None
assert why is not None and 'provider' in why.lower()
# The REAL shape, mimicked. Coordinator, 2026-09-01, quoting the actual file
# on the machine this ships to (`~/.dsh/settings.yaml`, line 407ff): an `id:`
# list item and its `input:` list are SEVERAL LINES APART, separated by other
# keys, inside a deeply nested `providers: [...] -> models: [...]` structure
# -- not the flat `mapping-key: {input: [...]}` shape the old line-scan
# heuristic (and the tests that used to live here) assumed. This fixture is
# that shape, and it is what the parser is actually judged against now.
_REAL_SETTINGS_FIXTURE = (
'version: 1\n'
'providers:\n'
' - name: commandcode\n'
' base_url: https://example.invalid\n'
' models:\n'
' - id: deepseek/deepseek-v4-flash\n'
' display_name: DeepSeek V4 Flash\n'
' context_window: 128000\n'
' pricing:\n'
' input_per_mtok: 0.1\n'
' output_per_mtok: 0.3\n'
' input:\n'
' - text\n'
' - id: deepseek/deepseek-v4-flash-vision-exp\n'
' display_name: DeepSeek V4 Flash Vision (Experimental)\n'
' context_window: 128000\n'
' pricing:\n'
' input_per_mtok: 0.15\n'
' output_per_mtok: 0.4\n'
' off_peak_discount: 0.5\n'
' capabilities:\n'
' - vision\n'
' - tool_use\n'
' input:\n'
' - text\n'
' - image\n'
' output:\n'
' - text\n'
)
def test_scan_settings_finds_the_real_deeply_nested_entry(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_SETTINGS_FIXTURE, encoding='utf-8')
model, provider, why = aiextract_run._scan_settings_for_vision_model(
str(settings))
assert model == 'deepseek/deepseek-v4-flash-vision-exp'
assert provider == 'commandcode' # from the provider's `name:`
assert why is None
def test_resolve_model_finds_the_real_entry_end_to_end(tmp_path, monkeypatch):
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_SETTINGS_FIXTURE, encoding='utf-8')
monkeypatch.delenv(aiextract_run.MODEL_ENV, raising=False)
original_scan = aiextract_run._scan_settings_for_vision_model
monkeypatch.setattr(aiextract_run, '_scan_settings_for_vision_model',
lambda: original_scan(str(settings)))
model, provider, why = aiextract_run.resolve_model({})
assert model == 'deepseek/deepseek-v4-flash-vision-exp'
assert provider == 'commandcode'
assert why is None
# The OTHER real shape. Confirmed live, 2026-09-03, reading this machine's
# actual `~/.dsh/settings.yaml`: `providers` there is not a list at all --
# it is a MAPPING keyed by provider id (`llm-pi-ai: providers: {commandcode:
# {models: [...]}, opencode: {...}}`), and `commandcode` (the dict key) is
# exactly the string `session.selectModel` needs as `provider`. Both shapes
# have to resolve a provider id; `_iter_model_entries` is what tolerates
# either (see its docstring).
_REAL_DICT_KEYED_PROVIDERS_FIXTURE = (
'llm-pi-ai:\n'
' providers:\n'
' commandcode:\n'
' displayName: Command Code API\n'
' baseURL: https://api.commandcode.ai/provider/v1\n'
' models:\n'
' - id: deepseek/deepseek-v4-flash\n'
' name: DeepSeek V4 Flash (latest)\n'
' - id: deepseek/deepseek-v4-flash-vision-exp\n'
' name: DeepSeek V4 Flash Vision (exp)\n'
' input:\n'
' - text\n'
' - image\n'
)
def test_scan_settings_finds_the_provider_id_when_providers_is_a_dict(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_DICT_KEYED_PROVIDERS_FIXTURE, encoding='utf-8')
model, provider, why = aiextract_run._scan_settings_for_vision_model(
str(settings))
assert model == 'deepseek/deepseek-v4-flash-vision-exp'
assert provider == 'commandcode' # the dict KEY, not a `name:`
assert why is None
def test_scan_settings_picks_the_vision_named_id_when_several_qualify(tmp_path):
# Two entries both declare image input; only one's id says "vision" --
# that one wins without asking, per the owner's own naming convention.
settings = tmp_path / 'settings.yaml'
settings.write_text(
'providers:\n'
' - models:\n'
' - id: some/multimodal-model\n'
' input: [text, image]\n'
' - id: some/vision-model\n'
' input: [text, image]\n',
encoding='utf-8')
model, _provider, why = aiextract_run._scan_settings_for_vision_model(
str(settings))
assert model == 'some/vision-model'
assert why is None
def test_scan_settings_refuses_to_guess_between_two_candidates(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(
'providers:\n'
' - models:\n'
' - id: vendor/vision-a\n'
' input: [text, image]\n'
' - id: vendor/vision-b\n'
' input: [text, image]\n',
encoding='utf-8')
model, provider, why = aiextract_run._scan_settings_for_vision_model(
str(settings))
assert model is None
assert provider is None
assert 'vendor/vision-a' in why and 'vendor/vision-b' in why
def test_scan_settings_missing_file_says_so(tmp_path):
model, provider, why = aiextract_run._scan_settings_for_vision_model(
str(tmp_path / 'does-not-exist.yaml'))
assert model is None
assert provider is None
assert 'no settings file' in why
def test_scan_settings_ignores_entries_with_id_but_no_image_input(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_SETTINGS_FIXTURE, encoding='utf-8')
model, _provider, _why = aiextract_run._scan_settings_for_vision_model(
str(settings))
assert model != 'deepseek/deepseek-v4-flash' # the text-only sibling
def test_scan_settings_malformed_yaml_says_so_not_raises(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text('providers: [this is not: valid: yaml: at all\n',
encoding='utf-8')
model, provider, why = aiextract_run._scan_settings_for_vision_model(
str(settings))
assert model is None
assert provider is None
assert 'could not parse' in why
# --------------------------------------------------- _find_provider_for_model_id
def test_find_provider_for_model_id_dict_keyed_shape(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_DICT_KEYED_PROVIDERS_FIXTURE, encoding='utf-8')
provider, why = aiextract_run._find_provider_for_model_id(
'deepseek/deepseek-v4-flash', str(settings))
assert provider == 'commandcode'
assert why is None
def test_find_provider_for_model_id_ignores_whether_it_declares_image_input(
tmp_path):
# Unlike `_scan_settings_for_vision_model`, this does not care about
# vision -- it is asked about a model the CALLER already chose (an
# explicit job/env override), and just needs to know who serves it.
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_DICT_KEYED_PROVIDERS_FIXTURE, encoding='utf-8')
provider, why = aiextract_run._find_provider_for_model_id(
'deepseek/deepseek-v4-flash-vision-exp', str(settings))
assert provider == 'commandcode'
assert why is None
def test_find_provider_for_model_id_not_found_says_so(tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(_REAL_DICT_KEYED_PROVIDERS_FIXTURE, encoding='utf-8')
provider, why = aiextract_run._find_provider_for_model_id(
'no/such-model', str(settings))
assert provider is None
assert 'no/such-model' in why
def test_find_provider_for_model_id_missing_file_says_so(tmp_path):
provider, why = aiextract_run._find_provider_for_model_id(
'anything', str(tmp_path / 'does-not-exist.yaml'))
assert provider is None
assert 'no settings file' in why
def test_find_provider_for_model_id_refuses_to_guess_between_two_providers(
tmp_path):
settings = tmp_path / 'settings.yaml'
settings.write_text(
'providers:\n'
' vendor-a:\n'
' models:\n'
' - id: shared/model-id\n'
' vendor-b:\n'
' models:\n'
' - id: shared/model-id\n',
encoding='utf-8')
provider, why = aiextract_run._find_provider_for_model_id(
'shared/model-id', str(settings))
assert provider is None
assert 'vendor-a' in why and 'vendor-b' in why
# --------------------------------------------------------------------- _rpc_value
#
# The bug: a real WPA5/sample_job run got `DshError: session.create did not
# return a session id` on a call that had, in fact, fully succeeded --
# `create_session` was reading `result.sessionId` when the real reply nests
# the id two deep, at `result.value.sessionId`, and the session it could not
# see was left orphaned, live, on the dsh server. This section pins the
# EXACT captured response (the regression) and every malformed variant the
# task called out, each naming the layer that failed rather than crashing.
_REAL_SESSION_CREATE_RESPONSE = {
'type': 'server-response',
'rpcId': '3d7ad657a9b4a83a5234cfd5ce185c7',
'result': {'ok': True, 'value': {
'sessionId': 'session-5969e01e-abe1-4182-a7a3-6c2f594c5419',
'agentPreset': 'standard'}},
}
def test_rpc_value_parses_the_exact_captured_session_create_response():
# THE REGRESSION. This is the literal response from the failed WPA5 run
# (owner report, 2026-09-01), byte-for-byte down to the rpcId.
value = aiextract_run._rpc_value(_REAL_SESSION_CREATE_RESPONSE,
'session.create')
assert value == {
'sessionId': 'session-5969e01e-abe1-4182-a7a3-6c2f594c5419',
'agentPreset': 'standard'}
def test_create_session_parses_the_exact_captured_response():
# Same 'result' as the real captured reply, echoing whatever rpcId this
# call actually sends (create_session itself has no control over the
# rpcId `_rpc` generates, so the transport must echo it, same as every
# other fake transport in this file -- only the 'result' body below is
# the real, captured one).
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'],
'result': _REAL_SESSION_CREATE_RESPONSE['result']}
session_id = aiextract_run.create_session('http://fake', 30, transport)
assert session_id == 'session-5969e01e-abe1-4182-a7a3-6c2f594c5419'
def test_rpc_value_refuses_when_result_ok_is_false():
res = {'rpcId': 'x', 'result': {'ok': False, 'value': {'sessionId': 'a'}}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run._rpc_value(res, 'session.create')
assert 'result.ok' in str(exc.value)
def test_rpc_value_refuses_when_value_is_missing():
res = {'rpcId': 'x', 'result': {'ok': True}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run._rpc_value(res, 'session.create')
assert 'result.value' in str(exc.value)
def test_rpc_value_refuses_when_value_is_not_an_object():
res = {'rpcId': 'x', 'result': {'ok': True, 'value': 'not-an-object'}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run._rpc_value(res, 'session.create')
assert 'result.value' in str(exc.value)
def test_rpc_value_refuses_when_result_is_missing_entirely():
res = {'rpcId': 'x'}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run._rpc_value(res, 'session.create')
assert '"result"' in str(exc.value)
def test_create_session_refuses_clearly_on_the_old_flat_wrong_shape():
# The shape the FIRST version of this file (wrongly) expected --
# result.sessionId, one level, no 'ok'/'value' at all. Must refuse
# clearly (naming which check failed), never crash with a raw KeyError
# or silently return something wrong.
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'sessionId': 'sess-1'}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run.create_session('http://fake', 30, transport)
assert 'result.ok' in str(exc.value)
def test_create_session_names_sessionid_when_value_has_no_usable_one():
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'],
'result': {'ok': True, 'value': {'agentPreset': 'standard'}}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run.create_session('http://fake', 30, transport)
assert 'sessionId' in str(exc.value)
def test_create_session_refuses_an_empty_string_sessionid():
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'],
'result': {'ok': True, 'value': {'sessionId': ' '}}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run.create_session('http://fake', 30, transport)
assert 'sessionId' in str(exc.value)
# ----------------------------------------------------------- cancel_session
def test_cancel_session_returns_true_when_the_rpc_succeeds():
calls = []
def transport(_url, body, _timeout):
calls.append(body)
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {}}}
assert aiextract_run.cancel_session('http://fake', 'sess-1', 30,
transport) is True
assert calls[0]['method'] == 'session.cancel'
assert calls[0]['payload'] == {'sessionId': 'sess-1'}
def test_cancel_session_swallows_errors_and_returns_false():
def transport(_url, _body, _timeout):
raise aiextract_run.DshTimeout('no reply')
assert aiextract_run.cancel_session('http://fake', 'sess-1', 30,
transport) is False
def test_run_cancels_the_session_when_something_fails_after_it_was_created(
tmp_path):
# The residual orphan-session risk: create_session succeeds and returns
# a real, trusted id, but something LATER in the same pass fails (here,
# the reply never settles). `run()` must attempt a best-effort
# session.cancel with that id before writing the failure note.
spec = _blank_spec()
job = _job(tmp_path, spec)
cancel_calls = []
def transport(_url, body, _timeout):
method = body['method']
rpc_id = body['rpcId']
if method == 'session.create':
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'sessionId': 'sess-orphan-check'}}}
if method == 'session.selectModel':
payload = body['payload']
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'selected': {'provider': payload['provider'],
'model': payload['model']}}}}
if method == 'session.history':
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'messages': []}}}
if method == 'session.prompt':
return {'rpcId': rpc_id, 'result': {'ok': True, 'value': {}}}
if method == 'session.cancel':
cancel_calls.append(body['payload'])
return {'rpcId': rpc_id, 'result': {'ok': True, 'value': {}}}
raise AssertionError('unexpected method: ' + method)
# A real fake clock, not real wall time: `job.get('timeout_s') or
# DEFAULT_TIMEOUT_S` means 0 would silently fall back to the 180s
# default (falsy-0 is the same trap as everywhere else `or` guards a
# possibly-zero setting), so this uses a small positive timeout plus a
# stepped fake clock/sleep -- the same pattern `_FakeClock` exists for --
# rather than actually waiting.
clock = _FakeClock()
job['timeout_s'] = 5
result = aiextract_run.run(job, transport=transport, sleep=clock.sleep,
clock=clock.time)
assert result['ok'] is False
assert cancel_calls == [{'sessionId': 'sess-orphan-check'}]
def test_run_never_half_merges_on_the_exact_captured_session_create_shape(
tmp_path):
# THE NEVER-HALF-MERGE GUARANTEE, confirmed on THIS specific failure
# mode: a session.create reply shaped exactly like the real WPA5 capture
# (see _REAL_SESSION_CREATE_RESPONSE above) but with a corrupted 'ok'
# flag, so it still fails the same way the original bug would have if
# the fix regressed. The regex-written spec's field values, and
# source.extractor, must come through byte-identical -- apply_failure_note's
# contract from yesterday, re-confirmed against today's failure mode.
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
spec['thickness_in'] = 8.0
job = _job(tmp_path, spec)
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'],
'result': {'ok': False, 'value': {
'sessionId': 'session-5969e01e-abe1-4182-a7a3-6c2f594c5419',
'agentPreset': 'standard'}}}
result = aiextract_run.run(job, transport=transport, sleep=lambda _s: None)
assert result['ok'] is False
written = _read_spec(job['spec_path'])
assert written['concrete']['fc_psi'] == 6000
assert written['thickness_in'] == 8.0
assert written['source']['extractor'] == 'regex' # never restamped
assert 'ai_filled' not in written['source'] # never even added
assert len(written['unresolved']) == 1
assert written['unresolved'][0]['text'] == aiextract_run.FAILURE_REASON_PREFIX
assert 'result.ok' in written['unresolved'][0]['reason']
def test_run_does_not_attempt_a_cancel_when_session_create_itself_failed(
tmp_path):
# No session id was ever obtained, so there is nothing safe to cancel --
# attempting one here would mean guessing an id, which cancel_session's
# own docstring says this script must not do.
spec = _blank_spec()
job = _job(tmp_path, spec)
calls = []
def transport(_url, body, _timeout):
calls.append(body['method'])
raise aiextract_run.DshTimeout('dsh unreachable')
result = aiextract_run.run(job, transport=transport, sleep=lambda _s: None)
assert result['ok'] is False
assert 'session.cancel' not in calls
# --------------------------------------------------------------- send_prompt
def test_send_prompt_sends_the_real_measured_payload_shape():
# MEASURED live against dsh, 2026-09-03: this is the exact shape that
# was missing before -- the old code sent {'sessionId', 'text'} and dsh
# answered 'invalid payload for session.prompt' on every run.
calls = []
def transport(_url, body, _timeout):
calls.append(body)
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {}}}
aiextract_run.send_prompt('http://fake', 'sess-1', 'hello model', 30,
transport)
assert calls[0]['method'] == 'session.prompt'
assert calls[0]['payload'] == {
'sessionId': 'sess-1',
'mode': 'queue',
'content': [{'type': 'text', 'text': 'hello model'}],
}
# --------------------------------------------------------------- select_model
#
# THE ACTUAL FIX: `session.create`'s real payload schema
# (`sessionCreateRequestSchema`, dsh-host-apiproxy) has no `model` field at
# all -- `create_session` no longer sends one (see its own docstring and the
# `test_create_session_sends_no_model_field_at_all` test below).
# `session.selectModel` is the separate RPC that actually pins a session's
# model, and `provider` is a required field of its payload.
def test_create_session_sends_no_model_field_at_all():
calls = []
def transport(_url, body, _timeout):
calls.append(body)
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'sessionId': 'sess-1', 'agentPreset': 'standard'}}}
aiextract_run.create_session('http://fake', 30, transport)
assert calls[0]['method'] == 'session.create'
assert 'model' not in calls[0]['payload']
def test_select_model_sends_the_measured_payload_shape():
# MEASURED live against dsh, 2026-09-03: session.create -> session.
# selectModel({provider: "commandcode", model:
# "deepseek/deepseek-v4-flash-vision-exp"}) -> session.models
# immediately echoed that exact pair back as `current`.
calls = []
def transport(_url, body, _timeout):
calls.append(body)
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'selected': {'provider': 'commandcode',
'model': 'deepseek/deepseek-v4-flash-vision-exp'}}}}
value = aiextract_run.select_model(
'http://fake', 'sess-1', 'commandcode',
'deepseek/deepseek-v4-flash-vision-exp', 30, transport)
assert calls[0]['method'] == 'session.selectModel'
assert calls[0]['payload'] == {
'sessionId': 'sess-1',
'provider': 'commandcode',
'model': 'deepseek/deepseek-v4-flash-vision-exp',
}
assert value['selected']['provider'] == 'commandcode'
assert value['selected']['model'] == \
'deepseek/deepseek-v4-flash-vision-exp'
# ---------------------------------------------------------- verify_session_model
#
# THE CHECK THIS TASK WAS OPENED TO ADD: neither `session.create`'s response
# nor `session.selectModel`'s own response is proof a turn actually ran on
# the requested model -- both only echo what was asked for. The
# `request/context` history event is what says what a turn ACTUALLY used.
def test_verify_session_model_passes_when_context_matches():
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'events': [_context_event(
'commandcode', 'deepseek/deepseek-v4-flash-vision-exp')],
'hasMore': False}}}
provider, model = aiextract_run.verify_session_model(
'http://fake', 'sess-1', 'commandcode',
'deepseek/deepseek-v4-flash-vision-exp', 30, transport)
assert provider == 'commandcode'
assert model == 'deepseek/deepseek-v4-flash-vision-exp'
def test_verify_session_model_raises_naming_both_on_mismatch():
# THE BUG THIS CHECK EXISTS FOR: a session can accept selectModel and
# still actually run on the account's default model -- this is the one
# place that would have caught the original failure (a vision request
# silently served by deepseek-v4-flash, no image input).
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'events': [_context_event(
'commandcode', 'deepseek/deepseek-v4-flash')],
'hasMore': False}}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run.verify_session_model(
'http://fake', 'sess-1', 'commandcode',
'deepseek/deepseek-v4-flash-vision-exp', 30, transport)
message = str(exc.value)
assert 'deepseek/deepseek-v4-flash-vision-exp' in message # requested
assert 'deepseek/deepseek-v4-flash' in message # actual
assert 'commandcode' in message
def test_verify_session_model_raises_when_no_context_event_seen_yet():
# "could not tell" must never pass as "must be fine" -- same discipline
# as everywhere else in this codebase (CLAUDE.md's `knowledge.same_face()`
# example).
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'events': [], 'hasMore': False}}}
with pytest.raises(aiextract_run.DshError) as exc:
aiextract_run.verify_session_model(
'http://fake', 'sess-1', 'commandcode', 'some-model', 30,
transport)
assert 'request/context' in str(exc.value)
def test_verify_session_model_uses_the_last_context_event_when_several():
# A turn can carry more than one request/context event (compaction, a
# retry); the LAST one is what the turn actually settled on.
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'events': [
_context_event('commandcode', 'deepseek/deepseek-v4-flash'),
_context_event('commandcode',
'deepseek/deepseek-v4-flash-vision-exp'),
],
'hasMore': False}}}
provider, model = aiextract_run.verify_session_model(
'http://fake', 'sess-1', 'commandcode',
'deepseek/deepseek-v4-flash-vision-exp', 30, transport)
assert provider == 'commandcode'
assert model == 'deepseek/deepseek-v4-flash-vision-exp'
def test_extract_context_events_ignores_events_of_other_types():
value = {'events': [
{'event': {'type': 'turn/start', 'data': {}}},
{'event': {'type': 'request/context', 'data': {
'provider': 'commandcode', 'model': 'm-1'}}},
{'event': {'type': 'request/header', 'data': {
'header': {'config': {'provider': 'commandcode',
'model': 'm-1'}}}}},
]}
events = aiextract_run._extract_context_events(value)
assert events == [{'provider': 'commandcode', 'model': 'm-1'}]
# ----------------------------------------------------------- _history_messages
def test_history_messages_projects_the_real_events_shape():
# user/message keeps its text; assistant/message keeps only its "text"
# blocks and drops "reasoning" ones; assistant/chunk (a streaming delta)
# is ignored entirely, same as every other event type not named here.
value = {
'events': [
{'event': {'type': 'turn/start', 'data': {}}},
{'event': {'type': 'user/message', 'data': {
'content': [{'type': 'text', 'text': 'please read this'}]}}},
{'event': {'type': 'assistant/chunk', 'data': {
'chunk': {'type': 'text-delta', 'text': 'partial...'}}}},
{'event': {'type': 'assistant/message', 'data': {
'turn': 1, 'step': 1, 'message': {
'role': 'assistant',
'content': [
{'type': 'reasoning', 'text': 'thinking it through'},
{'type': 'text', 'text': 'the settled answer'},
]}}}},
],
'hasMore': False,
'projections': {},
}
messages = aiextract_run._history_messages(value)
assert messages == [
{'role': 'user', 'text': 'please read this'},
{'role': 'assistant', 'text': 'the settled answer'},
]
def test_history_messages_still_tolerates_the_old_guessed_shape():
value = {'messages': [{'role': 'assistant', 'content': 'old-shape text'}]}
messages = aiextract_run._history_messages(value)
assert messages == [{'role': 'assistant', 'text': 'old-shape text'}]
# ------------------------------------------------------------- session_running
def test_session_running_true_when_the_session_is_listed_and_running():
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'items': [{'sessionId': 'sess-1', 'running': True}]}}}
assert aiextract_run.session_running(
'http://fake', 'sess-1', 30, transport) is True
def test_session_running_false_once_the_turn_has_finished():
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'items': [{'sessionId': 'sess-1', 'running': False}]}}}
assert aiextract_run.session_running(
'http://fake', 'sess-1', 30, transport) is False
def test_session_running_none_when_the_session_is_not_listed():
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'items': [{'sessionId': 'some-other-session', 'running': True}]}}}
assert aiextract_run.session_running(
'http://fake', 'sess-1', 30, transport) is None
# --------------------------------------------------------------- wait_for_reply
class _FakeClock(object):
def __init__(self, start=0.0, step=2.0):
self.now = start
self.step = step
def time(self):
return self.now
def sleep(self, _interval):
self.now += self.step
def test_wait_for_reply_times_out_when_nothing_ever_arrives():
clock = _FakeClock()
def transport(_url, body, _timeout):
return {'rpcId': body['rpcId'],
'result': {'ok': True, 'value': {'messages': []}}}
with pytest.raises(aiextract_run.DshTimeout):
aiextract_run.wait_for_reply('http://fake', 'sess-1', 0, timeout_s=5,
transport=transport, sleep=clock.sleep,
clock=clock.time, poll_interval=1)
def test_wait_for_reply_returns_once_session_list_says_not_running():
# THE REAL DONE-SIGNAL (measured 2026-09-03): session.history carries no
# status/state field at all -- session.list's `running` flag is what
# decides. This used to assert a 'status' key on session.history's value
# decided things; that was the old, wrong guess (the same one that
# produced the 'invalid payload for session.prompt' failure on every
# prior run) and is rewritten here to the measured contract.
clock = _FakeClock()
calls = {'n': 0}
def transport(_url, body, _timeout):
method = body['method']
if method == 'session.history':
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'events': [_assistant_event('the reply text')],
'hasMore': False, 'projections': {}}}}
if method == 'session.list':
calls['n'] += 1
running = calls['n'] == 1
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'items': [{'sessionId': 'sess-1', 'running': running}]}}}
raise AssertionError('unexpected method: ' + method)
text = aiextract_run.wait_for_reply('http://fake', 'sess-1', 0, timeout_s=30,
transport=transport, sleep=clock.sleep,
clock=clock.time, poll_interval=1)
assert text == 'the reply text'
assert calls['n'] >= 2
def test_wait_for_reply_uses_stability_when_session_list_cannot_tell():
# session_running() returns None (session not listed here at all) --
# wait_for_reply must fall back to the old stability heuristic rather
# than treating "can't tell" as "still running" or "done".
clock = _FakeClock()
def transport(_url, body, _timeout):
method = body['method']
if method == 'session.history':
return {'rpcId': body['rpcId'], 'result': {'ok': True, 'value': {
'events': [_assistant_event('stable text')],
'hasMore': False, 'projections': {}}}}
if method == 'session.list':
return {'rpcId': body['rpcId'],
'result': {'ok': True, 'value': {'items': []}}}
raise AssertionError('unexpected method: ' + method)
text = aiextract_run.wait_for_reply('http://fake', 'sess-1', 0, timeout_s=30,
transport=transport, sleep=clock.sleep,
clock=clock.time, poll_interval=1)
assert text == 'stable text'
# ------------------------------------------------------------------ run()
#
# End to end, with the HTTP boundary faked -- see the module docstring.
def _assistant_event(text, turn=1, step=1):
"""One `assistant/message` history event, in the real measured shape."""
return {'event': {'type': 'assistant/message', 'data': {
'turn': turn, 'step': step,
'message': {'role': 'assistant',
'content': [{'type': 'text', 'text': text}]}}}}
def _context_event(provider, model):
"""One `request/context` history event -- MEASURED live, 2026-09-03,
against a real dsh: the event that says what a turn ACTUALLY ran on,
which `verify_session_model()` checks before `run()` ever trusts a
reply. See the module docstring's point 4.
"""
return {'event': {'type': 'request/context', 'data': {
'provider': provider, 'model': model, 'contextWindow': 1000000}}}
class _FakeDsh(object):
"""Scripted responses for session.create / .prompt / .history / .list.
`history_batches` is a list of event-lists returned in order, one per
`session.history` call (repeating the last entry once exhausted). The
session is reported `running: True` on `session.list` until the LAST
batch has been served, then `running: False` -- that is the real
done-signal (`session.history` itself carries no status field at all,
per the measured 2026-09-03 contract), so `wait_for_reply` settles once
the last batch is out and a non-empty candidate exists.
"""
def __init__(self, history_batches, session_id='sess-1'):
self.history_batches = history_batches
self.session_id = session_id
self.calls = []
self._history_calls = 0
def transport(self, url, body, _timeout):
self.calls.append((url, body))
method = body['method']
rpc_id = body['rpcId']
# The real dsh envelope, confirmed against a live session.create
# reply (owner, WPA5/sample_job run, 2026-09-01) -- see _rpc_value's
# docstring: 'result' -> 'ok' / 'value', not a flat 'result'.
if method == 'session.create':
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'sessionId': self.session_id, 'agentPreset': 'standard'}}}
if method == 'session.selectModel':
payload = body['payload']
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'selected': {'provider': payload['provider'],
'model': payload['model']}}}}
if method == 'session.prompt':
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'accepted': True}}}
if method == 'session.history':
idx = min(self._history_calls, len(self.history_batches) - 1)
self._history_calls += 1
return {'rpcId': rpc_id, 'result': {'ok': True, 'value':
{'events': self.history_batches[idx], 'hasMore': False,
'projections': {}}}}
if method == 'session.list':
served_last = self._history_calls >= len(self.history_batches)
return {'rpcId': rpc_id, 'result': {'ok': True, 'value': {
'items': [{'sessionId': self.session_id,
'running': not served_last}]}}}
if method == 'session.cancel':
return {'rpcId': rpc_id, 'result': {'ok': True, 'value': {}}}
raise AssertionError('unexpected dsh method: ' + method)
def _job(tmp_path, spec, evidence=None):
spec_path = tmp_path / 'PNL1.yaml'
_write_spec(str(spec_path), spec)
evidence_path = tmp_path / 'PNL1.evidence.json'
with io.open(str(evidence_path), 'w', encoding='utf-8') as fh:
fh.write(json.dumps(evidence or {'design_number': 'PNL1'}))
return {
'spec_path': str(spec_path),
'evidence_path': str(evidence_path),
'model': 'test-vision-model',
'provider': 'test-provider',
'dsh_base_url': 'http://fake-dsh',
'timeout_s': 30,
'datum_clause': 'the LEFT rail (datum = "left_rail").',
'image_paths': [],
}
_REPLY_JSON = {'concrete': {'fci_psi': 3500},
'confidence': {'concrete.fci_psi': 'high'}}
_REPLY_TEXT = ('```json\n' + json.dumps(_REPLY_JSON) + '\n```\n\n'
'Summary: filled fci_psi from the drawing.')
def test_run_success_merges_and_stamps_provenance(tmp_path):
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
job = _job(tmp_path, spec)
fake = _FakeDsh(history_batches=[
[], # baseline
[_context_event('test-provider', 'test-vision-model'), # settled
_assistant_event(_REPLY_TEXT)],
])
result = aiextract_run.run(job, transport=fake.transport,
sleep=lambda _s: None)
assert result['ok'] is True
# 'confidence' is metadata ABOUT a filled field, not a field in its own
# right -- it never appears in `filled` itself, only alongside it.
assert result['filled'] == ['concrete.fci_psi']
written = _read_spec(job['spec_path'])
assert written['concrete']['fc_psi'] == 6000 # untouched
assert written['concrete']['fci_psi'] == 3500 # filled
assert written['source']['extractor'] == 'dsv4f-vision'
assert 'concrete.fci_psi' in written['source']['ai_filled']
def test_run_failure_leaves_spec_field_values_untouched(tmp_path):
spec = _blank_spec()
spec['concrete']['fc_psi'] = 6000
spec['concrete']['fci_psi'] = None
job = _job(tmp_path, spec)
def failing_transport(_url, _body, _timeout):
raise aiextract_run.DshTimeout('connection refused')
result = aiextract_run.run(job, transport=failing_transport,
sleep=lambda _s: None)
assert result['ok'] is False
written = _read_spec(job['spec_path'])
assert written['concrete']['fc_psi'] == 6000
assert written['concrete']['fci_psi'] is None
assert written['source']['extractor'] == 'regex' # not restamped
assert len(written['unresolved']) == 1
assert written['unresolved'][0]['text'] == aiextract_run.FAILURE_REASON_PREFIX
def test_run_unparsable_reply_is_a_failure_not_a_silent_success(tmp_path):
spec = _blank_spec()
job = _job(tmp_path, spec)
fake = _FakeDsh(history_batches=[
[],
[_context_event('test-provider', 'test-vision-model'),
_assistant_event('I looked, but no JSON here.')],
])
result = aiextract_run.run(job, transport=fake.transport,
sleep=lambda _s: None)
assert result['ok'] is False
written = _read_spec(job['spec_path'])
assert written['source']['extractor'] == 'regex'
assert any(e['text'] == aiextract_run.FAILURE_REASON_PREFIX
for e in written['unresolved'])
def test_run_rejects_a_reply_stamped_with_the_wrong_rpc_id(tmp_path):
spec = _blank_spec()
job = _job(tmp_path, spec)
def mismatched_transport(_url, body, _timeout):
if body['method'] == 'session.create':
return {'rpcId': 'not-the-id-that-was-sent',
'result': {'sessionId': 'sess-1'}}
return {'rpcId': body['rpcId'], 'result': {'messages': []}}
result = aiextract_run.run(job, transport=mismatched_transport,
sleep=lambda _s: None)
assert result['ok'] is False
written = _read_spec(job['spec_path'])
assert written['source']['extractor'] == 'regex'