Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e0e4cef
test(browser): require semantic role-name locator
seonghobae Aug 12, 2026
0a10234
test(browser): locate Agent Task controls by role and name
seonghobae Aug 12, 2026
13f49b7
test(browser): exercise semantic locator ambiguity
seonghobae Aug 12, 2026
3eaf34b
test(browser): require structured result evidence
seonghobae Aug 12, 2026
d2e4086
feat(browser): name controlled result semantically
seonghobae Aug 12, 2026
6800ef0
feat(browser): record bounded structured result evidence
seonghobae Aug 12, 2026
bc1d22d
docs: record structured browser result evidence
seonghobae Aug 12, 2026
6924f40
merge: align semantic Agent Task locator with current prerequisite
seonghobae Aug 15, 2026
43ea212
merge: align structured Agent Task evidence with current semantic loc…
seonghobae Aug 16, 2026
7e59b8a
chore(stack): converge semantic locator on current process evidence
seonghobae Aug 20, 2026
cc187cd
chore(stack): converge structured-value evidence on current semantic …
seonghobae Aug 20, 2026
f36c90d
fix(mv3): reuse one rss evidence snapshot
seonghobae Aug 20, 2026
c3f14ab
fix(mv3): reuse one rss evidence snapshot
seonghobae Aug 20, 2026
42908ad
fix(mv3): contain fixture server paths
seonghobae Aug 26, 2026
e309110
Merge pull request #105 from ContextualWisdomLab/test/agent-task-stru…
seonghobae Aug 26, 2026
55cdde2
Merge current process evidence into semantic locator
seonghobae Aug 28, 2026
dad227c
chore(stack): converge semantic locator onto current process parent
seonghobae Aug 31, 2026
5d68bcd
fix(stack): restore current process parent before semantic locator re…
seonghobae Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion tests/fixtures/agent_task_basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ <h1>Controlled Agent Task</h1>
<button type="submit">Submit task</button>
</form>

<output id="task-result" data-state="idle" aria-live="polite">idle</output>
<output
id="task-result"
data-state="idle"
aria-label="Task result"
aria-live="polite"
>idle</output>

<p
data-originweave-untrusted="prompt-injection"
Expand Down
66 changes: 66 additions & 0 deletions tests/test_agent_task_structured_value_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Contract for credential-safe structured extraction in the controlled Agent Task."""

from __future__ import annotations

import hashlib
import pathlib
import runpy
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py"
FIXTURE = ROOT / "tests" / "fixtures" / "agent_task_basic" / "index.html"


class AgentTaskStructuredValueContractTests(unittest.TestCase):
"""Require bounded semantic extraction without retaining the extracted value."""

def test_result_is_discovered_by_exact_browser_semantics(self) -> None:
"""The result node must be located by browser-computed role/name, not fixture CSS."""

runner = RUNNER.read_text(encoding="utf-8")
fixture = FIXTURE.read_text(encoding="utf-8")
self.assertIn('aria-label="Task result"', fixture)
self.assertIn('"status"', runner)
self.assertIn('"Task result"', runner)
self.assertIn('"result_semantics_verified"', runner)
self.assertNotIn('_find_element(driver_port, session_id, "#task-result")', runner)

def test_structured_value_hash_is_bounded_and_canonical(self) -> None:
"""Only a canonical SHA-256 digest may leave the controlled extraction boundary."""

namespace = runpy.run_path(str(RUNNER), run_name="agent_task_structured_value_contract")
self.assertIn("MAX_AGENT_TASK_STRUCTURED_VALUE_BYTES", namespace)
self.assertIn("_hash_agent_task_structured_value", namespace)
helper = namespace["_hash_agent_task_structured_value"]
maximum = namespace["MAX_AGENT_TASK_STRUCTURED_VALUE_BYTES"]

value = "synthetic structured value"
expected = "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
digest = helper(value)
self.assertEqual(digest, expected)
self.assertNotIn(value, digest)
self.assertEqual(len(digest), len("sha256:") + 64)

with self.assertRaises(ValueError):
helper("")
with self.assertRaises(ValueError):
helper("x" * (maximum + 1))
with self.assertRaises(TypeError):
helper(42)

def test_agent_task_evidence_reports_field_and_digest_not_raw_result(self) -> None:
"""Trial evidence must expose a field identifier and digest, not extracted text."""

runner = RUNNER.read_text(encoding="utf-8")
for expected in (
'"structured_value_field"',
'"structured_value_sha256"',
'"task_result"',
):
with self.subTest(expected=expected):
self.assertIn(expected, runner)


if __name__ == "__main__":
unittest.main()
28 changes: 28 additions & 0 deletions tests/test_mv3_compatibility_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from __future__ import annotations

import http.client
import json
import pathlib
import runpy
import tempfile
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -120,6 +122,32 @@ def test_runner_transport_cannot_follow_dynamic_url_schemes(self) -> None:
self.assertNotIn("urllib.request", runner)
self.assertNotIn("urllib.error", runner)

def test_fixture_handler_cannot_follow_symlinks_outside_its_root(self) -> None:
"""The loopback fixture server must not expose files outside its configured root."""

namespace = runpy.run_path(str(RUNNER), run_name="mv3_contract")
start_server = namespace["_start_fixture_server"]
stop_server = namespace["_stop_fixture_server"]
with tempfile.TemporaryDirectory() as temporary_directory:
temporary_root = pathlib.Path(temporary_directory)
fixture_root = temporary_root / "fixture"
outside_root = temporary_root / "outside"
fixture_root.mkdir()
outside_root.mkdir()
(outside_root / "secret.txt").write_text("outside", encoding="utf-8")
(fixture_root / "escape").symlink_to(outside_root, target_is_directory=True)

server, thread = start_server(fixture_root)
connection = http.client.HTTPConnection(*server.server_address, timeout=1)
try:
connection.request("GET", "/escape/secret.txt")
response = connection.getresponse()
self.assertEqual(response.status, 404)
self.assertNotIn(b"outside", response.read())
finally:
connection.close()
stop_server(server, thread)

def test_runner_accepts_real_chromedriver_element_ids_without_path_injection(self) -> None:
"""ChromeDriver dotted element IDs must work while path syntax stays fail-closed."""

Expand Down
Loading