mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(scan): symlink, unsupported-language and unparsed-file gaps are owned by what they name
This extends dbcb230 to the remaining graph-integrity gaps. In the independent
evaluation these gaps were repository-wide:
- alerta: one symlink, `alerta/app.wsgi`, which is not even source, made 261 of 261
questions unresolved.
- wger and Signal K: a language no frontend reads did the same.
- YesWiki: unparsed files did the same.
Now:
- the partition build records the paths a symlink boundary skipped (a directory
ends in `/`) and the languages no frontend reads;
- `files_unparsed` carries its full paths;
- the scan demotes only questions about an owned path or under an owned directory,
and only questions in an owned language.
Every gap stays in the report as a boundary. A gap that names nothing keeps its
partition-wide or repository-wide reach. Tier-0 pruning stays off, as before.
The tests that pinned repository-wide reach now assert ownership. The one about a
vendor exclusion masking an integrity failure keeps its intent, using a census gap
that names no file.
Retune after protocol-v1; exploratory only.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
949977c7a6
commit
65dcb46b5e
@@ -3,7 +3,7 @@
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
from test_model_scope import Backend, region, vector
|
||||
from test_model_scope import Backend, _CensusGapBackend, region, vector
|
||||
|
||||
from openultrasast.model.contracts import ChangeContext, ChangedSpan, LineCorrespondence
|
||||
from openultrasast.model.scan import ScanBudget, scan_repository
|
||||
@@ -233,7 +233,7 @@ def test_an_unresolved_context_projection_still_demotes_the_question(tmp_path):
|
||||
assert result.question_outcomes[0].reason == "change_context_incomplete"
|
||||
|
||||
|
||||
def scan_with_layout(tmp_path, *, vendor=False, symlink=False, second_language=False):
|
||||
def scan_with_layout(tmp_path, *, vendor=False, symlink=False, second_language=False, unnamed_census=False):
|
||||
"""Exercise the real partition boundaries rather than injecting a reason."""
|
||||
(tmp_path / "source.php").write_text("readable\n")
|
||||
if vendor:
|
||||
@@ -247,7 +247,7 @@ def scan_with_layout(tmp_path, *, vendor=False, symlink=False, second_language=F
|
||||
return scan_repository(
|
||||
tmp_path,
|
||||
[region("source.php", "php")],
|
||||
backend=Backend({"source.php": rows}),
|
||||
backend=_CensusGapBackend(None, evidence={"source.php": rows}) if unnamed_census else Backend({"source.php": rows}),
|
||||
change_context=context(),
|
||||
)
|
||||
|
||||
@@ -259,17 +259,24 @@ def test_a_declared_vendor_exclusion_does_not_invalidate_a_first_party_answer(tm
|
||||
assert result.question_outcomes[0].status == "completed"
|
||||
|
||||
|
||||
def test_an_unresolved_symlink_still_demotes_the_answer(tmp_path):
|
||||
def test_a_skipped_symlink_is_owned_by_its_own_path(tmp_path):
|
||||
"""alerta's one symlink, `alerta/app.wsgi` -- not even source -- made all 261 questions unresolved. The link
|
||||
is skipped, so the graph is incomplete about THAT path; an answer about a file it read is still an answer.
|
||||
The boundary stays reported, and it names the path."""
|
||||
result = scan_with_layout(tmp_path, symlink=True)
|
||||
assert "symlink_context_unresolved" in result.scope.unresolved_boundaries
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert result.question_outcomes[0].reason == "graph_incomplete"
|
||||
gap = [d for d in result.degradations if d.get("reason") == "symlink_context_unresolved"]
|
||||
assert gap and gap[0].get("owned_paths") == ["linked.php"], result.degradations
|
||||
assert result.question_outcomes[0].status == "completed"
|
||||
|
||||
|
||||
def test_a_declared_exclusion_beside_an_integrity_failure_still_demotes(tmp_path):
|
||||
result = scan_with_layout(tmp_path, vendor=True, symlink=True)
|
||||
assert {"vendor_semantics_unresolved", "symlink_context_unresolved"} <= set(result.scope.unresolved_boundaries)
|
||||
"""A vendor exclusion must not mask a real integrity failure: a census gap naming no file reaches its
|
||||
whole partition, excluded dependencies or not."""
|
||||
result = scan_with_layout(tmp_path, vendor=True, unnamed_census=True)
|
||||
assert {"vendor_semantics_unresolved", "partition_file_census_unavailable"} <= set(result.scope.unresolved_boundaries)
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert result.question_outcomes[0].reason == "graph_incomplete"
|
||||
|
||||
|
||||
class FamilyBackend(Backend):
|
||||
|
||||
@@ -192,11 +192,18 @@ def test_missing_graph_files_disable_safe_pruning_and_completion(tmp_path):
|
||||
cpg.unparsed = ("missing.py",)
|
||||
return cpg
|
||||
|
||||
result = scan_repository(source(tmp_path, "a.py"), [region("a.py")], backend=Partial({"a.py": vector(False)}), ranking_mode="evidence")
|
||||
assert len(result.scope.selected) == 1
|
||||
result = scan_repository(
|
||||
source(tmp_path, "a.py", "missing.py"),
|
||||
[region("a.py"), region("missing.py")],
|
||||
backend=Partial({"a.py": vector(False), "missing.py": vector(False)}),
|
||||
ranking_mode="evidence",
|
||||
)
|
||||
# Pruning stays off: the unparsed file may hold the very sink a vector did not see.
|
||||
assert len(result.scope.selected) == 2
|
||||
assert result.scope.deferred == ()
|
||||
assert result.question_outcomes[0].status == "unresolved"
|
||||
assert result.family_coverage[0].completed == 0
|
||||
# Completion is owned by the file: the unparsed one is unresolved, the one the graph read is answered.
|
||||
status = {o.identity.path: o.status for o in result.question_outcomes}
|
||||
assert status == {"a.py": "completed", "missing.py": "unresolved"}, status
|
||||
|
||||
|
||||
def test_cli_sample_uses_deferred_order_not_scanned_count(tmp_path):
|
||||
@@ -433,3 +440,16 @@ def test_a_census_gap_that_names_no_file_still_reaches_its_partition(tmp_path):
|
||||
regions = [region("app.py", "python"), region("other.py", "python")]
|
||||
result = scan_repository(root, regions, backend=_CensusGapBackend(None, evidence=evidence), population_complete=True)
|
||||
assert all(o.status == "unresolved" for o in result.question_outcomes), [(o.status, o.reason) for o in result.question_outcomes]
|
||||
|
||||
|
||||
def test_a_language_no_frontend_reads_demotes_only_its_own_questions(tmp_path):
|
||||
"""wger's Python questions all went unresolved because the repository also holds files of a language no
|
||||
frontend reads. That gap is about THAT language; it is reported, and it names the language."""
|
||||
root = source(tmp_path, "app.py")
|
||||
(tmp_path / "tools").mkdir()
|
||||
(tmp_path / "tools" / "main.go").write_text("package main\n")
|
||||
result = scan_repository(root, [region("app.py")], backend=Backend({"app.py": vector()}), population_complete=True)
|
||||
gap = [d for d in result.degradations if d.get("reason") == "frontend_unsupported"]
|
||||
assert gap and gap[0].get("census_language") == "go", result.degradations
|
||||
assert "frontend_unsupported" in result.scope.unresolved_boundaries
|
||||
assert result.question_outcomes[0].status == "completed", result.question_outcomes
|
||||
|
||||
Reference in New Issue
Block a user