Fail closed on invalid Glas profiles
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a028de-e2c8-7732-8521-46a7fc5db82f
This commit is contained in:
parent
561538c95e
commit
b933cf52c8
12 changed files with 455 additions and 9 deletions
|
|
@ -54,6 +54,38 @@ def test_action_field_path_interpolation_resolves_context_value() -> None:
|
|||
]
|
||||
|
||||
|
||||
def test_action_carries_declared_profile_and_renders_attribution_refs() -> None:
|
||||
rules = [
|
||||
{
|
||||
"id": "profiled-task",
|
||||
"condition": "",
|
||||
"action": {
|
||||
"task_template": "Run controlled task",
|
||||
"target_repo": "activity-core",
|
||||
"harness_profile_ref": "harness.agent-dev@1.0.0",
|
||||
"approach_hint": "legacy-coding",
|
||||
"execution_refs": {
|
||||
"correlation_id": "context.request.correlation_id",
|
||||
"goal_refs": ["context.request.goal_ref"],
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
specs = expand_rule_actions(
|
||||
rules,
|
||||
_Event(),
|
||||
{"request": {"correlation_id": "corr-7", "goal_ref": "goal:7@1"}},
|
||||
)
|
||||
|
||||
assert specs[0]["harness_profile_ref"] == "harness.agent-dev@1.0.0"
|
||||
assert specs[0]["approach_hint"] == "legacy-coding"
|
||||
assert specs[0]["execution_refs"] == {
|
||||
"correlation_id": "corr-7",
|
||||
"goal_refs": ["goal:7@1"],
|
||||
}
|
||||
|
||||
|
||||
def test_for_each_binds_each_list_item_before_condition_and_action_rendering() -> None:
|
||||
rules = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -100,6 +100,14 @@ class TestNoFallbackToApproachHint:
|
|||
|
||||
assert profile == "harness.agent-dev@2.1.0"
|
||||
|
||||
def test_unknown_but_well_formed_profile_is_left_for_glas(self) -> None:
|
||||
profile, _ = resolve_execution_selector(
|
||||
"harness.not-in-any-local-catalog@9.9.9",
|
||||
None,
|
||||
)
|
||||
|
||||
assert profile == "harness.not-in-any-local-catalog@9.9.9"
|
||||
|
||||
def test_blank_hint_is_normalised_away(self) -> None:
|
||||
assert resolve_execution_selector(None, " ") == (None, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -175,3 +175,134 @@ async def test_emit_tasks_raises_when_sink_fails(monkeypatch) -> None:
|
|||
}
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emit_tasks_refuses_malformed_profile_before_any_side_effect(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
from temporalio.exceptions import ApplicationError
|
||||
|
||||
monkeypatch.setattr(
|
||||
activities,
|
||||
"get_issue_sink",
|
||||
lambda: pytest.fail("IssueSink must not be opened after profile refusal"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
activities,
|
||||
"_get_session_factory",
|
||||
lambda: pytest.fail("DB must not be opened after profile refusal"),
|
||||
)
|
||||
|
||||
with pytest.raises(ApplicationError, match="must pin a version") as exc_info:
|
||||
await activities.emit_tasks(
|
||||
{
|
||||
"activity_id": "00000000-0000-0000-0000-000000000001",
|
||||
"triggering_event_id": "scheduled",
|
||||
"task_specs": [
|
||||
{
|
||||
"title": "Must not emit",
|
||||
"source_type": "rule",
|
||||
"source_id": "malformed-profile",
|
||||
"harness_profile_ref": "harness.agent-dev",
|
||||
"approach_hint": "legacy-must-not-rescue",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert exc_info.value.non_retryable is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emit_tasks_refuses_absent_profile_in_strict_mode(monkeypatch) -> None:
|
||||
from temporalio.exceptions import ApplicationError
|
||||
|
||||
monkeypatch.setenv("ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE", "true")
|
||||
monkeypatch.setattr(
|
||||
activities,
|
||||
"get_issue_sink",
|
||||
lambda: pytest.fail("IssueSink must not be opened after profile refusal"),
|
||||
)
|
||||
|
||||
with pytest.raises(ApplicationError, match="cannot substitute"):
|
||||
await activities.emit_tasks(
|
||||
{
|
||||
"activity_id": "00000000-0000-0000-0000-000000000001",
|
||||
"triggering_event_id": "scheduled",
|
||||
"task_specs": [
|
||||
{
|
||||
"title": "Must not emit",
|
||||
"source_type": "rule",
|
||||
"source_id": "missing-profile",
|
||||
"approach_hint": "legacy-must-not-rescue",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emit_tasks_passes_unknown_versioned_profile_to_queue(monkeypatch) -> None:
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
class SuccessfulSink:
|
||||
def emit(self, task_spec: TaskSpec) -> TaskRef:
|
||||
return TaskRef(external_id="state-hub:progress-1", backend="state-hub")
|
||||
|
||||
class FakeTransaction:
|
||||
async def __aenter__(self) -> None:
|
||||
return None
|
||||
|
||||
async def __aexit__(self, *exc_info: object) -> bool:
|
||||
return False
|
||||
|
||||
class FakeSession:
|
||||
def begin(self) -> FakeTransaction:
|
||||
return FakeTransaction()
|
||||
|
||||
async def __aenter__(self) -> "FakeSession":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc_info: object) -> bool:
|
||||
return False
|
||||
|
||||
def add(self, row: object) -> None:
|
||||
return None
|
||||
|
||||
class FakeSessionFactory:
|
||||
def __call__(self) -> FakeSession:
|
||||
return FakeSession()
|
||||
|
||||
async def fake_create_ops_run(session, spec, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(activities, "get_issue_sink", lambda: SuccessfulSink())
|
||||
monkeypatch.setattr(activities, "_get_session_factory", lambda: FakeSessionFactory())
|
||||
monkeypatch.setattr(activities, "create_ops_run_from_spec", fake_create_ops_run)
|
||||
|
||||
refs = await activities.emit_tasks(
|
||||
{
|
||||
"activity_id": "00000000-0000-0000-0000-000000000001",
|
||||
"triggering_event_id": "event-1",
|
||||
"task_specs": [
|
||||
{
|
||||
"title": "Let Glas resolve it",
|
||||
"source_type": "rule",
|
||||
"source_id": "profiled",
|
||||
"harness_profile_ref": "harness.unknown-locally@9.9.9",
|
||||
"approach_hint": "legacy-only",
|
||||
"execution_refs": {
|
||||
"correlation_id": "corr-9",
|
||||
"api_key": "must-be-dropped",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
assert refs == ["state-hub:progress-1"]
|
||||
assert captured["harness_profile_ref"] == "harness.unknown-locally@9.9.9"
|
||||
assert captured["approach_hint"] == "legacy-only"
|
||||
assert captured["execution_refs"] == {"correlation_id": "corr-9"}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import uuid
|
||||
|
||||
from activity_core.definition_parser import scan_and_parse
|
||||
import pytest
|
||||
|
||||
from activity_core.definition_parser import ParseError, parse_file, scan_and_parse
|
||||
from activity_core.models import ActivityDefinition
|
||||
from activity_core.sync_activity_definitions import _definition_uuid
|
||||
|
||||
|
|
@ -95,3 +97,94 @@ context_sources:
|
|||
assert definition.context_sources[0]["params"]["evidence_sinks"][0]["type"] == (
|
||||
"state-hub-progress"
|
||||
)
|
||||
|
||||
|
||||
def _write_profiled_definition(tmp_path, action_lines: str) -> None:
|
||||
(tmp_path / "profiled.md").write_text(
|
||||
f"""---
|
||||
id: profiled
|
||||
name: Profiled
|
||||
enabled: true
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "0 9 * * *"
|
||||
---
|
||||
|
||||
```rule
|
||||
id: emit
|
||||
condition: ""
|
||||
action:
|
||||
task_template: Run it
|
||||
{action_lines}
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_definition_parse_normalises_a_versioned_profile(tmp_path) -> None:
|
||||
_write_profiled_definition(
|
||||
tmp_path,
|
||||
" harness_profile_ref: ' harness.agent-dev@1.0.0 '\n",
|
||||
)
|
||||
|
||||
definition = parse_file(tmp_path / "profiled.md")
|
||||
|
||||
assert definition.rules[0]["action"]["harness_profile_ref"] == (
|
||||
"harness.agent-dev@1.0.0"
|
||||
)
|
||||
|
||||
|
||||
def test_definition_parse_rejects_a_malformed_profile(tmp_path) -> None:
|
||||
_write_profiled_definition(
|
||||
tmp_path,
|
||||
" harness_profile_ref: harness.agent-dev\n",
|
||||
)
|
||||
|
||||
with pytest.raises(ParseError, match="must pin a version"):
|
||||
parse_file(tmp_path / "profiled.md")
|
||||
|
||||
|
||||
def test_definition_parse_requires_rule_profile_in_strict_mode(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
_write_profiled_definition(tmp_path, "")
|
||||
monkeypatch.setenv("ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE", "true")
|
||||
|
||||
with pytest.raises(ParseError, match="must declare harness_profile_ref"):
|
||||
parse_file(tmp_path / "profiled.md")
|
||||
|
||||
|
||||
def test_strict_mode_does_not_require_profile_for_deterministic_report(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
path = tmp_path / "report.md"
|
||||
path.write_text(
|
||||
"""---
|
||||
id: report
|
||||
name: Report only
|
||||
enabled: true
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "0 9 * * *"
|
||||
---
|
||||
|
||||
```instruction
|
||||
id: report-only
|
||||
trusted_fields: []
|
||||
model: deterministic
|
||||
prompt: Report
|
||||
output_schema: ""
|
||||
report_sinks:
|
||||
- type: state-hub-progress
|
||||
```
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("ACTIVITY_CORE_REQUIRE_HARNESS_PROFILE", "true")
|
||||
|
||||
definition = parse_file(path)
|
||||
|
||||
assert definition.instructions[0]["id"] == "report-only"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue