diff --git a/eval/tests/test_proposer_sandbox.py b/eval/tests/test_proposer_sandbox.py index 70f4fe76b..2c4c37259 100644 --- a/eval/tests/test_proposer_sandbox.py +++ b/eval/tests/test_proposer_sandbox.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import os +import shutil import stat import subprocess import sys @@ -19,11 +20,13 @@ from workflow_bench.process_control import ManagedProcessResult, run_managed from workflow_bench.proposer_sandbox import ( MAX_BUNDLE_BYTES, MAX_EVIDENCE_FILE_BYTES, + SANDBOX_NODE, SANDBOX_PYTHON3, SANDBOX_SHELL_PREFIX, SANDBOX_USER_SKILLS, ReadOnlyMount, SandboxError, + _runtime_mount_args, build_claude_settings, build_sandbox_environment, prepare_sandbox, @@ -191,6 +194,36 @@ def test_sandbox_command_has_minimal_mounts_and_no_host_root_bind(tmp_path: Path assert not private_root.exists() +def test_runtime_mounts_bind_the_resolved_node_to_a_fresh_sandbox_path(monkeypatch) -> None: + # sanitized_graph.py and runner_sessions.py invoke the sandboxed graph CLI + # via SANDBOX_NODE. node's real host location varies (GitHub-hosted + # runner images happen to have one under /usr/local/bin; a self-hosted + # runner's actions/setup-node installs into its own tool-cache directory + # instead), so this must bind to a FRESH sandbox path like /opt/claude/... + # rather than anywhere under /usr, /bin, /lib, or /lib64: those are + # already read-only bound by this same function, and bwrap can't create + # a new mount-point file inside an already-read-only tree when the real + # path doesn't already exist there on the host (observed empirically: + # "bwrap: Can't create file at /usr/local/bin/node: Read-only file + # system" when this bind first targeted that path on a self-hosted + # runner where node isn't really there). + monkeypatch.setattr( + "workflow_bench.proposer_sandbox.shutil.which", + lambda name: "/opt/hostedtoolcache/node/22.18.0/x64/bin/node" if name == "node" else None, + ) + args = _runtime_mount_args() + node_index = args.index("/opt/hostedtoolcache/node/22.18.0/x64/bin/node") + assert args[node_index - 1] == "--ro-bind" + assert args[node_index + 1] == SANDBOX_NODE + assert not any(SANDBOX_NODE.startswith(bound + "/") for bound in ("/usr", "/bin", "/lib", "/lib64")) + + +def test_runtime_mounts_skip_the_node_bind_when_node_is_unresolvable(monkeypatch) -> None: + monkeypatch.setattr("workflow_bench.proposer_sandbox.shutil.which", lambda name: None) + args = _runtime_mount_args() + assert SANDBOX_NODE not in args + + def test_stricter_prefix_freezes_evaluated_skills_and_can_unshare_network(tmp_path: Path) -> None: clone = tmp_path / "clone" skill = clone / ".claude" / "skills" / "gitnexus-work" @@ -219,6 +252,43 @@ def test_stricter_prefix_freezes_evaluated_skills_and_can_unshare_network(tmp_pa assert prefix[user_index - 2] == "--ro-bind" +@pytest.mark.skipif( + os.environ.get("GITNEXUS_REQUIRE_BWRAP_CANARY") != "1", + reason="real Bubblewrap canary is mandatory in the named Ubuntu CI job", +) +def test_real_bubblewrap_runs_node_from_outside_the_bound_trees(tmp_path: Path, monkeypatch) -> None: + # Reproduces the self-hosted-runner failure directly: node resolved from + # a path outside /usr, /bin, /lib, /lib64 (actions/setup-node's own + # tool-cache convention) must still be reachable inside the sandbox at + # SANDBOX_NODE. A real node copied to a fresh, non-system location stands + # in for the tool-cache install; argv-construction tests alone can't + # catch a bwrap-level "Can't create file ...: Read-only file system" + # (the actual error this fix resolves), only a real bwrap invocation can. + real_node = shutil.which("node") + if not real_node: + pytest.skip("no node on PATH to relocate for this canary") + toolcache = tmp_path / "toolcache" + toolcache.mkdir() + relocated_node = toolcache / "node" + shutil.copy2(real_node, relocated_node) + relocated_node.chmod(0o755) + # Only fake "node"'s resolution -- prepare_sandbox's own bwrap/claude + # lookups (_resolve_executable) also go through shutil.which, and must + # keep resolving for real or preflight fails before the sandbox is even + # built. + real_which = shutil.which + monkeypatch.setattr( + "workflow_bench.proposer_sandbox.shutil.which", + lambda name: str(relocated_node) if name == "node" else real_which(name), + ) + + clone = tmp_path / "clone" + clone.mkdir() + with prepare_sandbox(clone=clone, claude_bin=Path(sys.executable), preflight=True) as sandbox: + result = sandbox.run([SANDBOX_NODE, "--version"], timeout=10) + assert result.ok, result.stderr_tail + + @pytest.mark.skipif( os.environ.get("GITNEXUS_REQUIRE_BWRAP_CANARY") != "1", reason="real Bubblewrap canary is mandatory in the named Ubuntu CI job", @@ -772,4 +842,3 @@ for line in sys.stdin: assert bash_result.get("is_error") is not True, bash_result assert (clone / "bash-called").read_text() == "canary" assert (clone / "mcp-called").read_text() == "ok" - diff --git a/eval/tests/test_workflow_bench_sessions.py b/eval/tests/test_workflow_bench_sessions.py index e03104ed3..c10afa401 100644 --- a/eval/tests/test_workflow_bench_sessions.py +++ b/eval/tests/test_workflow_bench_sessions.py @@ -438,11 +438,11 @@ def test_real_bubblewrap_runtime_mount_imports_cli_without_exposing_checkout(tmp preflight=True, ) as sandbox: visibility = sandbox.run( - ["/usr/local/bin/node", "-e", visibility_script], + [runner.SANDBOX_NODE, "-e", visibility_script], timeout=10, ) imported = sandbox.run( - ["/usr/local/bin/node", runner.SANDBOX_GITNEXUS_ENTRYPOINT, "--version"], + [runner.SANDBOX_NODE, runner.SANDBOX_GITNEXUS_ENTRYPOINT, "--version"], timeout=10, ) # --version never reaches the `analyze` command, which is loaded via a @@ -451,7 +451,7 @@ def test_real_bubblewrap_runtime_mount_imports_cli_without_exposing_checkout(tmp # resolve-analyze-cmd.cjs. Require the compiled analyze module # directly so this canary actually exercises that chain. analyze_imported = sandbox.run( - ["/usr/local/bin/node", "-e", f"require('{runner.SANDBOX_GITNEXUS}/dist/cli/analyze.js')"], + [runner.SANDBOX_NODE, "-e", f"require('{runner.SANDBOX_GITNEXUS}/dist/cli/analyze.js')"], timeout=10, ) diff --git a/eval/workflow_bench/proposer_sandbox.py b/eval/workflow_bench/proposer_sandbox.py index 731f11ad2..2884eb067 100644 --- a/eval/workflow_bench/proposer_sandbox.py +++ b/eval/workflow_bench/proposer_sandbox.py @@ -27,6 +27,7 @@ SANDBOX_TMP = "/tmp" SANDBOX_CLAUDE = "/opt/claude/claude" SANDBOX_SHELL_PREFIX = "/opt/claude/shell-prefix" SANDBOX_PYTHON3 = "/opt/claude/python3" +SANDBOX_NODE = "/opt/claude/node" SANDBOX_PATH = "/opt/claude:/usr/local/bin:/usr/bin:/bin" SANDBOX_GITNEXUS = "/opt/gitnexus" SANDBOX_GITNEXUS_SHARED = "/opt/gitnexus-shared" @@ -354,6 +355,20 @@ def _runtime_mount_args() -> list[str]: path = Path(raw) if path.exists(): args += ["--ro-bind", raw, raw] + # sanitized_graph.py and runner_sessions.py invoke the sandboxed graph + # CLI via SANDBOX_NODE. Bind whatever `node` actually resolves to on PATH + # there -- true node location varies by host (GitHub-hosted runner images + # happen to have one under /usr/local/bin; a self-hosted runner's + # actions/setup-node installs into its own tool-cache directory instead). + # Target must be a fresh path like /opt/claude/... rather than anywhere + # under /usr, /bin, /lib, or /lib64: those are already read-only bound + # above, and bwrap can't create a new mount-point file inside an + # already-read-only tree when the real path doesn't already exist there + # (the exact case a self-hosted runner hits, and the reason this bind + # exists at all). + node_bin = shutil.which("node") + if node_bin: + args += ["--ro-bind", node_bin, SANDBOX_NODE] for raw in ( "/etc/ssl", "/etc/hosts", @@ -398,7 +413,7 @@ def _create_python3_wrapper(private_root: Path) -> Path: """ wrapper = private_root / "python3" - wrapper.write_text("#!/bin/bash\nset -eu\nexec /usr/bin/python3 \"$@\"\n") + wrapper.write_text('#!/bin/bash\nset -eu\nexec /usr/bin/python3 "$@"\n') wrapper.chmod(0o500) return wrapper diff --git a/eval/workflow_bench/runner.py b/eval/workflow_bench/runner.py index b502c8023..d67934cd0 100644 --- a/eval/workflow_bench/runner.py +++ b/eval/workflow_bench/runner.py @@ -78,6 +78,7 @@ from .proposer_sandbox import ( SANDBOX_GITNEXUS as SANDBOX_GITNEXUS, SANDBOX_GITNEXUS_REGISTRY, SANDBOX_GITNEXUS_SHARED as SANDBOX_GITNEXUS_SHARED, + SANDBOX_NODE as SANDBOX_NODE, SANDBOX_WORKSPACE, ReadOnlyMount, SandboxError, diff --git a/eval/workflow_bench/runner_sessions.py b/eval/workflow_bench/runner_sessions.py index 53e7335ee..cc1708572 100644 --- a/eval/workflow_bench/runner_sessions.py +++ b/eval/workflow_bench/runner_sessions.py @@ -18,6 +18,7 @@ from .proposer_sandbox import ( SANDBOX_GITNEXUS, SANDBOX_GITNEXUS_REGISTRY, SANDBOX_HOME, + SANDBOX_NODE, SANDBOX_TMP, SANDBOX_WORKSPACE, SandboxError, @@ -50,6 +51,8 @@ def measured_cost(raw: Any) -> float | None: if not math.isfinite(raw) or raw < 0: return None return float(raw) + + SANDBOX_GITNEXUS_ENTRYPOINT = f"{SANDBOX_GITNEXUS}/dist/cli/index.js" SENSITIVE_EVENT_KEYS = frozenset( { @@ -113,7 +116,7 @@ def sandbox_mcp_config() -> str: "PATH=/usr/local/bin:/usr/bin:/bin", "LANG=C.UTF-8", "GIT_TERMINAL_PROMPT=0", - "/usr/local/bin/node", + SANDBOX_NODE, SANDBOX_GITNEXUS_ENTRYPOINT, "mcp", ], diff --git a/eval/workflow_bench/sanitized_graph.py b/eval/workflow_bench/sanitized_graph.py index d0e9418dd..23339b1c7 100644 --- a/eval/workflow_bench/sanitized_graph.py +++ b/eval/workflow_bench/sanitized_graph.py @@ -16,6 +16,7 @@ from .process_control import ManagedProcessError, run_managed from .proposer_sandbox import ( SANDBOX_GITNEXUS, SANDBOX_HOME, + SANDBOX_NODE, SANDBOX_WORKSPACE, ReadOnlyMount, SandboxError, @@ -248,7 +249,7 @@ def _run_graph_cli( ) -> bytes | None: command = [ *prefix, - "/usr/local/bin/node", + SANDBOX_NODE, SANDBOX_GITNEXUS_ENTRYPOINT, *arguments, ] diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index d1413db56..cab3765a4 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -89,7 +89,7 @@ "_rebaselined": "#1919 review CF3 fix: extended kotlin-local-property-owner (init/accessor destructuring) + new dart-accessor-owner fixture (getter/setter ownership). Fingerprint-only corpus drift; scaling ~1.0." }, "java": { - "fingerprint": "85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537", + "fingerprint": "d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686", "scaling_budget": 1.5, "_rebaselined_callable_flow_2522_review": "PR #2522 review hardening: callable operands retain expression/qualified identity and formals retain signature metadata; same-name lexical regions use an O(ancestor-depth) ID-set lookup. Prior d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a -> 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4; scaling 0.992 < 1.5.", "_rebaselined_callable_flow_2522_followup": "PR #2522 follow-up: Java method-reference/SAM callable flow facts with invocation-result suppression. Prior 062d754764aaa8a6772fb90875c710502a63e3e7a300e633942381ed914faada -> d5c59d7dc9e206637515d5aea1163f7c1cdd76410c38c5fe6143d13d19677d6a; scaling 1.074 < 1.5.", @@ -98,7 +98,8 @@ "_rebaselined_2522_review_fixes": "PR #2522 review fixes: get/test dropped from callableProtocolMethods. Prior 004a3592998dca1193bd1429a8284513725de7764f2a3eceedaaa984cfd763b4 -> f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67; scaling ratio re-verified within budget.", "_rebaselined_2550_instance_model": "PR #2549 (#2550): anonymous class bodies emit synthesized @declaration.class/@declaration.name (Worker$N), an @reference.inherits to the constructed type, and receiver @type-binding.* captures; six new java-* fixtures joined the corpus. Prior f3b4f4b6610e07c3ac90deb1c53d3572b6ad55a36e5d7134984876d30031ff67 -> d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90; scaling 1.058 < 1.5.", "_rebaselined_2555_enum_constant_bodies": "PR for #2555: enum constant bodies emit synthesized E$N classes + @reference.inherits to the host enum; anonymous naming follows JLS 13.1 immediately-enclosing-type chains INCLUDING anonymous enclosing types (NestHost$1$1, N$1$1); six new java-* fixtures joined the corpus. Prior d79c3b92acfc866094981499b977388ca14f90839bca0c040342ab1cec00aa90 -> 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca; scaling 1.05 < 1.5.", - "_rebaselined_2564_record_capture": "PR for #2564: JAVA_QUERIES gained a (record_declaration name: (identifier) @name) @definition.record capture, previously entirely missing (record_declaration had no structure-phase capture at all, unlike class/interface/enum) - a record's methods existed as ownerless Method nodes with no HAS_METHOD edge. Two new java-* fixtures (java-record-methods, java-new-expr-chain-call) joined the corpus. Prior 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca -> 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537; scaling 1.059 < 1.5." + "_rebaselined_2564_record_capture": "PR for #2564: JAVA_QUERIES gained a (record_declaration name: (identifier) @name) @definition.record capture, previously entirely missing (record_declaration had no structure-phase capture at all, unlike class/interface/enum) - a record's methods existed as ownerless Method nodes with no HAS_METHOD edge. Two new java-* fixtures (java-record-methods, java-new-expr-chain-call) joined the corpus. Prior 975b68aaac6d06094260fb0c67f9b1bc03692ba7220669d192aca9dccd5fc0ca -> 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537; scaling 1.059 < 1.5.", + "_rebaselined_2561_enum_constant_receiver": "PR for #2561: synthesizeJavaAnonymousClassDeclarations now emits a class-scope @type-binding.annotation/name/type per enum constant (constant simple name -> its E$N synthesized class when bodied, else the host enum) so E.CONST.method() resolves through the existing compound-receiver chain walk. Two drivers of the drift, both in the java-enum-constant-body fixture (this bench's corpus IS test/fixtures/lang-resolution): (1) one extra type-binding match per enum_constant from the capture change; (2) review follow-up added a body-less Plain.java enum + EnumConst.dispatchToConstant/dispatchInherited methods (bodied-override, inherited-via-MRO, and body-less dispatch call sites). The review's fail-safe hardening (bodied constant binds ONLY to E$N, never the host enum, when name synthesis fails on a malformed tree) is output-neutral on this well-formed corpus (verified: fingerprint identical with and without it). Prior 85fc7af9c3c1bceac76cb4f27214410b04967682a2eaa7e468e26efd1f4e2537 -> d04298a91beec76d0fa7099b3d71265723be60c1df688969aa954f135dd49686; scaling < 1.5." }, "typescript": { "fingerprint": "3280b13d3f9378ab23eee31c2edc779b5a9ae1e7bb510c23a24855b44406d2f4", diff --git a/gitnexus/src/core/ingestion/languages/java/captures.ts b/gitnexus/src/core/ingestion/languages/java/captures.ts index 3adb036c8..62d9fe788 100644 --- a/gitnexus/src/core/ingestion/languages/java/captures.ts +++ b/gitnexus/src/core/ingestion/languages/java/captures.ts @@ -372,23 +372,49 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode: SyntaxNode): Capture // constant's class extends its HOST ENUM (javac semantics), so the // inherits reference names the enum — giving `mroFor(E$N) ∋ E` and // keeping bare calls from the body to the enum's own helpers alive - // through the ownership gate's MRO arm. No receiver typeBinding piece: - // constants are not variable initializers; `E.A.hook()` dispatch rides - // the existing enum receiver machinery. + // through the ownership gate's MRO arm. for (const constant of rootNode.descendantsOfType('enum_constant')) { - const name = synthesizeJavaAnonymousClassName(constant); - if (name === undefined) continue; - const body = constant.childForFieldName?.('body'); - if (body === null || body === undefined || body.type !== 'class_body') continue; - out.push({ - '@declaration.class': nodeToCapture('@declaration.class', body), - '@declaration.name': syntheticCapture('@declaration.name', body, name), - }); const hostEnum = javaEnclosingEnumNameOf(constant); - if (hostEnum !== undefined) { + const bodyNode = constant.childForFieldName?.('body'); + const isBodied = bodyNode !== null && bodyNode !== undefined && bodyNode.type === 'class_body'; + const bodiedName = synthesizeJavaAnonymousClassName(constant); + if (bodiedName !== undefined && isBodied) { out.push({ - '@reference.inherits': nodeToCapture('@reference.inherits', body), - '@reference.name': syntheticCapture('@reference.name', body, hostEnum), + '@declaration.class': nodeToCapture('@declaration.class', bodyNode), + '@declaration.name': syntheticCapture('@declaration.name', bodyNode, bodiedName), + }); + if (hostEnum !== undefined) { + out.push({ + '@reference.inherits': nodeToCapture('@reference.inherits', bodyNode), + '@reference.name': syntheticCapture('@reference.name', bodyNode, hostEnum), + }); + } + } + + // Receiver dispatch (#2561): `E.CONST.method()` resolves through the + // generic compound-receiver chain walk, which looks up each dotted + // segment via the owning class scope's `typeBindings` map — the same + // mechanism a field declaration uses (`private User user;` binds + // `user` on the class scope). Binding the constant's own simple name + // there — to its synthesized `E$N` class when bodied (MRO includes E, + // so members inherited from the enum still resolve), or to the host + // enum itself when body-less — makes `E.CONST.method()` resolve with + // no changes to the shared receiver-binding machinery. + // + // A bodied constant binds ONLY to its `E$N` class, never the host enum: + // if name synthesis fails on a malformed/error-recovery tree (`bodiedName` + // undefined despite a real body), emit nothing rather than silently + // misattributing an OVERRIDING constant's receiver to the enum's own + // (non-overridden) method — a wrong edge is worse than no edge. Mirrors + // the `object_creation_expression` branch, which skips on synthesis + // failure. `hostEnum` is used only for genuinely body-less constants. + const constantNameNode = constant.childForFieldName?.('name'); + const constantType = isBodied ? bodiedName : hostEnum; + if (constantNameNode !== null && constantNameNode !== undefined && constantType !== undefined) { + out.push({ + '@type-binding.annotation': nodeToCapture('@type-binding.annotation', constant), + '@type-binding.name': nodeToCapture('@type-binding.name', constantNameNode), + '@type-binding.type': syntheticCapture('@type-binding.type', constant, constantType), }); } } diff --git a/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java index 65292b0c5..abfdeb902 100644 --- a/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java +++ b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/EnumConst.java @@ -21,4 +21,12 @@ class Unrelated { public void caller() { hook(); } + + public void dispatchToConstant() { + EnumConst.A.hook(); + } + + public void dispatchInherited() { + EnumConst.A.log(); + } } diff --git a/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/Plain.java b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/Plain.java new file mode 100644 index 000000000..87c750f61 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-enum-constant-body/src/Plain.java @@ -0,0 +1,13 @@ +public enum Plain { + A; + + public void m() { + System.out.println("plain m"); + } +} + +class PlainCaller { + public void callPlain() { + Plain.A.m(); + } +} diff --git a/gitnexus/test/integration/resolvers/java.test.ts b/gitnexus/test/integration/resolvers/java.test.ts index c7e0545c0..bfaa00196 100644 --- a/gitnexus/test/integration/resolvers/java.test.ts +++ b/gitnexus/test/integration/resolvers/java.test.ts @@ -3055,3 +3055,43 @@ describe('Java enum constant bodies (#2555)', () => { expect(misattributed).toBeUndefined(); }, 60000); }); + +// --------------------------------------------------------------------------- +// #2561: E.CONST.method() emits no CALLS edge — the receiver-side follow-up +// to #2555. A bodied constant's receiver must resolve to its synthesized +// E$N class; a body-less constant's receiver must resolve to the host enum +// itself. +// --------------------------------------------------------------------------- + +describe('Java enum-constant receiver dispatch (#2561)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-enum-constant-body'), () => {}); + }, 60000); + + it('resolves EnumConst.A.hook() to the bodied constant override (EnumConst$1.hook#0)', () => { + const calls = getRelationships(result, 'CALLS'); + const dispatch = calls.find((c) => c.source === 'dispatchToConstant' && c.target === 'hook'); + expect(dispatch).toBeDefined(); + expect(dispatch!.rel.targetId).toBe('Method:src/EnumConst.java:EnumConst$1.hook#0'); + }); + + it("resolves EnumConst.A.log() to the host enum's inherited method via E$N's MRO (EnumConst.log#0)", () => { + // A's body overrides hook() but NOT log(); log() lives only on the enum. + // The bodied constant's receiver binds to EnumConst$1, whose MRO includes + // EnumConst (via @reference.inherits), so the qualified call reaches the + // host enum's own method — the inherited-dispatch capability the fix enables. + const calls = getRelationships(result, 'CALLS'); + const dispatch = calls.find((c) => c.source === 'dispatchInherited' && c.target === 'log'); + expect(dispatch).toBeDefined(); + expect(dispatch!.rel.targetId).toBe('Method:src/EnumConst.java:EnumConst.log#0'); + }); + + it("resolves Plain.A.m() to the body-less constant's inherited enum method (Plain.m#0)", () => { + const calls = getRelationships(result, 'CALLS'); + const dispatch = calls.find((c) => c.source === 'callPlain' && c.target === 'm'); + expect(dispatch).toBeDefined(); + expect(dispatch!.rel.targetId).toBe('Method:src/Plain.java:Plain.m#0'); + }); +});