1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "sync"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "github.com/bnema/zut/packages/agent/subagents"
14 "github.com/bnema/zut/packages/core"
15 "github.com/bnema/zut/packages/provider"
16)
17
18func TestResidentStatusRetrievesFailedResultWithoutExecution(t *testing.T) {
19 failure := errors.New("worker failed")
20 manager := subagents.NewResidentManager(t.TempDir(), func(_ subagents.ResidentChildSpec, journal *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
21 return subagents.ResidentTurnRunner(func(context.Context, string) error {
22 if err := journal.RecordAgentEvent(core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "partial finding"}}}}); err != nil {
23 return err
24 }
25 return failure
26 }), nil
27 })
28 t.Cleanup(func() { _ = manager.Close(context.Background()) })
29 spec := subagents.ResidentChildSpec{ID: "failed-status", InitialTurnID: "initial", SessionID: "session", Provider: "openai", Model: "test", Required: true}
30 completed, cancel := manager.WatchCompletion(spec.ID, spec.InitialTurnID)
31 defer cancel()
32 if _, err := manager.Spawn(t.Context(), spec, "investigate"); err != nil {
33 t.Fatal(err)
34 }
35 select {
36 case <-completed:
37 case <-time.After(time.Second):
38 t.Fatal("no completion")
39 }
40 status := &SubagentStatusTool{ResidentManager: manager, Enabled: func() bool { return true }}
41 for _, include := range []bool{false, true} {
42 raw := json.RawMessage(`{"agent_id":"failed-status"}`)
43 if include {
44 raw = json.RawMessage(`{"agent_id":"failed-status","include_result":true}`)
45 }
46 result, err := status.Execute(t.Context(), raw, nil)
47 if err != nil || result.IsError {
48 t.Fatalf("status: %#v, %v", result, err)
49 }
50 response := result.Details.(subagentStatusResponse)
51 if response.Agent.State != subagents.ResidentFailed || (response.Result != nil) != include {
52 t.Fatalf("response = %#v", response)
53 }
54 if include && (response.Result.Handoff != "" || response.Result.ErrorCode != "turn_failed" || response.Result.Summary != "partial finding") {
55 t.Fatalf("result = %#v", response.Result)
56 }
57 }
58 result, err := status.Execute(t.Context(), json.RawMessage(`{"include_result":true}`), nil)
59 if err != nil || !result.IsError {
60 t.Fatalf("missing child ID must fail: %#v, %v", result, err)
61 }
62 if len(manager.UnmetRequired()) != 1 {
63 t.Fatal("reading a result satisfied required work")
64 }
65}
66
67func TestResidentToolsUseManagerOnly(t *testing.T) {
68 runs := make(chan string, 2)
69 manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
70 return subagents.ResidentTurnRunner(func(_ context.Context, prompt string) error { runs <- prompt; return nil }), nil
71 })
72 t.Cleanup(func() { _ = manager.Close(context.Background()) })
73 spawn := &SubagentSpawnTool{
74 ResidentManager: manager,
75 Enabled: func() bool { return true },
76 DefaultProvider: func() string { return "openai" },
77 DefaultModel: func() string { return "gpt-5" },
78 BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
79 return subagents.ResidentChildSpec{ID: "resident-tool", SessionID: "child-session", Provider: request.Provider, Model: request.Model, Required: request.Required}, nil
80 },
81 }
82 result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"initial"}`), nil)
83 if err != nil {
84 t.Fatal(err)
85 }
86 if got := toolResultText(t, result); !strings.Contains(got, "owns the delegated scope") || !strings.Contains(got, "Do not repeat that work in the parent") {
87 t.Fatalf("spawn result lacks ownership guidance: %q", got)
88 }
89 if got := <-runs; got != "initial" {
90 t.Fatalf("initial prompt = %q", got)
91 }
92 status := &SubagentStatusTool{ResidentManager: manager, Enabled: func() bool { return true }}
93 result, err = status.Execute(context.Background(), json.RawMessage(`{"agent_id":"resident-tool"}`), nil)
94 if err != nil {
95 t.Fatal(err)
96 }
97 if details, ok := result.Details.(subagentStatusResponse); !ok || details.Agent == nil || details.Agent.State == "" {
98 t.Fatalf("status details = %#v", result.Details)
99 }
100 resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
101 if _, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"resident-tool","prompt":"follow up"}`), nil); err != nil {
102 t.Fatal(err)
103 }
104 if got := <-runs; got != "follow up" {
105 t.Fatalf("follow-up prompt = %q", got)
106 }
107}
108
109func TestSubagentSpawnGuidanceRequiresIndependentOwnership(t *testing.T) {
110 facade := &SubagentTool{}
111 for _, want := range []string{"independent sidecar", "keep immediate blockers local", "never duplicate it in the parent", "end or yield the parent turn"} {
112 if got := facade.Description(); !strings.Contains(got, want) {
113 t.Fatalf("description missing %q: %s", want, got)
114 }
115 }
116 var schema struct {
117 Properties map[string]struct {
118 Description string `json:"description"`
119 } `json:"properties"`
120 }
121 if err := json.Unmarshal(facade.Schema(), &schema); err != nil {
122 t.Fatal(err)
123 }
124 task := schema.Properties["task"].Description
125 for _, want := range []string{"concrete, bounded scope", "explicit ownership", "does not overlap", "Shared isolation uses this working directory", "worktree isolation captures a patch without merging it"} {
126 if !strings.Contains(task, want) {
127 t.Fatalf("task schema missing %q: %s", want, task)
128 }
129 }
130}
131
132func TestResidentSpawnWaitReturnsInitialCompletion(t *testing.T) {
133 for _, tc := range []struct {
134 name string
135 err error
136 status string
137 }{
138 {"success", nil, "completed"},
139 {"failure", errors.New("worker failed"), "failed"},
140 {"cancellation", context.Canceled, "interrupted"},
141 {"cancellation with failure", errors.Join(context.Canceled, errors.New("worker failed")), "interrupted"},
142 } {
143 t.Run(tc.name, func(t *testing.T) {
144 manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
145 return subagents.ResidentTurnRunner(func(context.Context, string) error { return tc.err }), nil
146 })
147 t.Cleanup(func() { _ = manager.Close(context.Background()) })
148 spawn := &SubagentSpawnTool{
149 ResidentManager: manager,
150 Enabled: func() bool { return true },
151 DefaultProvider: func() string { return "openai" },
152 DefaultModel: func() string { return "gpt-5" },
153 BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
154 return subagents.ResidentChildSpec{ID: "wait-for-completion", SessionID: "child-session", Provider: request.Provider, Model: request.Model, Required: request.Required}, nil
155 },
156 }
157 result, err := spawn.Execute(t.Context(), json.RawMessage(`{"task":"finish now","wait":1,"required":true}`), nil)
158 if err != nil || result.IsError {
159 t.Fatalf("Execute = (%#v, %v)", result, err)
160 }
161 if got := toolResultText(t, result); !strings.Contains(got, "\nstate: "+tc.status+"\n") || !strings.Contains(got, "\nrequired: "+tc.status+"\n") {
162 t.Fatalf("wait result = %q, want %s state", got, tc.status)
163 }
164 if unmet := len(manager.UnmetRequired()); (unmet != 0) != (tc.err != nil) {
165 t.Fatalf("unmet required = %d, error = %v", unmet, tc.err)
166 }
167 })
168 }
169}
170
171func TestResidentSpawnWaitExpiryReportsQueuedChild(t *testing.T) {
172 started := make(chan struct{}, 1)
173 release := make(chan struct{})
174 var releaseOnce sync.Once
175 releaseBlocker := func() { releaseOnce.Do(func() { close(release) }) }
176 finished := make(chan string, 1)
177 manager := subagents.NewResidentManagerWithLimit(t.TempDir(), 1, func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
178 return subagents.ResidentTurnRunner(func(ctx context.Context, prompt string) error {
179 switch prompt {
180 case "blocker":
181 started <- struct{}{}
182 select {
183 case <-release:
184 return nil
185 case <-ctx.Done():
186 return ctx.Err()
187 }
188 case "waiter":
189 finished <- prompt
190 }
191 return nil
192 }), nil
193 })
194 t.Cleanup(func() {
195 releaseBlocker()
196 _ = manager.Close(context.Background())
197 })
198 spawn := &SubagentSpawnTool{
199 ResidentManager: manager,
200 Enabled: func() bool { return true },
201 DefaultProvider: func() string { return "openai" },
202 DefaultModel: func() string { return "gpt-5" },
203 BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
204 return subagents.ResidentChildSpec{ID: request.Task, SessionID: request.Task, Provider: request.Provider, Model: request.Model}, nil
205 },
206 }
207 if _, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"blocker"}`), nil); err != nil {
208 t.Fatalf("spawn blocker: %v", err)
209 }
210 select {
211 case <-started:
212 case <-time.After(5 * time.Second):
213 t.Fatal("blocker did not start")
214 }
215 result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"waiter","wait":1}`), nil)
216 if err != nil || result.IsError {
217 t.Fatalf("spawn waiter = (%#v, %v)", result, err)
218 }
219 if got := toolResultText(t, result); !strings.Contains(got, "state: queued") || !strings.Contains(got, "wait: timed out after 1 seconds") {
220 t.Fatalf("wait expiry result = %q, want queued timeout", got)
221 }
222 if state, ok := manager.State("waiter"); !ok || state != subagents.ResidentQueued {
223 t.Fatalf("waiter state = %q (found=%t), want queued", state, ok)
224 }
225 releaseBlocker()
226 select {
227 case got := <-finished:
228 if got != "waiter" {
229 t.Fatalf("finished task = %q, want waiter", got)
230 }
231 case <-time.After(5 * time.Second):
232 t.Fatal("queued waiter did not execute")
233 }
234}
235
236func TestResidentSpawnRejectsInvalidInputBeforeCreatingChild(t *testing.T) {
237 manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
238 return subagents.ResidentTurnRunner(func(context.Context, string) error { return nil }), nil
239 })
240 t.Cleanup(func() { _ = manager.Close(context.Background()) })
241 built := 0
242 spawn := &SubagentSpawnTool{
243 ResidentManager: manager,
244 Enabled: func() bool { return true },
245 BuildResidentSpec: func(context.Context, ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
246 built++
247 return subagents.ResidentChildSpec{ID: "should-not-exist", SessionID: "child"}, nil
248 },
249 }
250 for _, raw := range []string{
251 `{"task":"x","unexpected":true}`,
252 `{"task":"x"}{}`,
253 `{"task":"x","model":"gpt-5"}`,
254 `{"task":"x","wait":0}`,
255 `{"task":"x","wait":301}`,
256 `{"task":"x","isolation":"outside"}`,
257 `{"task":"x","budget_ratio":0.5}`,
258 `{"task":"x","budget_tokens":1000}`,
259 } {
260 result, err := spawn.Execute(context.Background(), json.RawMessage(raw), nil)
261 if err != nil && !strings.Contains(err.Error(), "invalid args") {
262 t.Fatalf("Execute(%s) error = %v", raw, err)
263 }
264 if err == nil && !result.IsError {
265 t.Fatalf("Execute(%s) = %#v, want protocol error", raw, result)
266 }
267 }
268 if built != 0 || len(manager.Snapshot()) != 0 {
269 t.Fatalf("invalid input reached resident manager: built=%d snapshots=%#v", built, manager.Snapshot())
270 }
271}
272
273func TestResidentSpawnProfileAndExplicitOverridesReachFactory(t *testing.T) {
274 manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
275 return subagents.ResidentTurnRunner(func(context.Context, string) error { return nil }), nil
276 })
277 t.Cleanup(func() { _ = manager.Close(context.Background()) })
278 profileFast := false
279 var got ResidentSpawnRequest
280 spawn := &SubagentSpawnTool{
281 ResidentManager: manager,
282 Enabled: func() bool { return true },
283 DefaultModel: func() string { return "host-model" },
284 DefaultProvider: func() string { return "host-provider" },
285 DefaultReasoning: func() string { return "medium" },
286 ResolveSubagent: func(name string) (*subagents.Profile, error) {
287 return &subagents.Profile{Name: name, Model: "openai/gpt-5.6-sol", Thinking: "low", FastMode: &profileFast}, nil
288 },
289 BuildResidentSpec: func(_ context.Context, request ResidentSpawnRequest) (subagents.ResidentChildSpec, error) {
290 got = request
291 return subagents.ResidentChildSpec{ID: "profiled", SessionID: "child", Provider: request.Provider, Model: request.Model, Profile: request.Profile.Name}, nil
292 },
293 }
294 result, err := spawn.Execute(context.Background(), json.RawMessage(`{"task":"review","agent":"reviewer","reasoning":"high","fast_mode":true}`), nil)
295 if err != nil || result.IsError {
296 t.Fatalf("Execute = (%#v, %v)", result, err)
297 }
298 if got.Model != "gpt-5.6-sol" || got.Provider != "openai" || got.Reasoning != "high" || got.FastMode == nil || !*got.FastMode {
299 t.Fatalf("factory request = %#v", got)
300 }
301}
302
303func TestResidentToolsRejectUnknownFields(t *testing.T) {
304 for _, tool := range []interface {
305 Execute(context.Context, json.RawMessage, func(string)) (core.ToolResult, error)
306 }{
307 &SubagentStatusTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
308 &SubagentStopTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
309 &SubagentResumeTool{ResidentManager: subagents.NewResidentManager(t.TempDir(), nil), Enabled: func() bool { return true }},
310 } {
311 _, err := tool.Execute(context.Background(), json.RawMessage(`{"unknown":true}`), nil)
312 if err == nil || !strings.Contains(err.Error(), "unknown field") {
313 t.Fatalf("unknown field error = %v", err)
314 }
315 }
316}
317
318// A resume wait subscribes to the accepted follow-up turn before it can finish,
319// so it returns that turn's completion instead of timing out.
320func TestResidentResumeWaitReturnsFollowUpCompletion(t *testing.T) {
321 manager := subagents.NewResidentManager(t.TempDir(), func(_ subagents.ResidentChildSpec, journal *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
322 return subagents.ResidentTurnRunner(func(_ context.Context, prompt string) error {
323 return journal.RecordAgentEvent(core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "answer for " + prompt}}}})
324 }), nil
325 })
326 t.Cleanup(func() { _ = manager.Close(context.Background()) })
327 initial, cancelInitial := manager.WatchCompletion("waited-resume", "initial-turn")
328 defer cancelInitial()
329 if _, err := manager.Spawn(context.Background(), subagents.ResidentChildSpec{ID: "waited-resume", InitialTurnID: "initial-turn", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}, "start"); err != nil {
330 t.Fatal(err)
331 }
332 select {
333 case <-initial:
334 case <-time.After(5 * time.Second):
335 t.Fatal("initial turn did not complete")
336 }
337 resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
338 result, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"waited-resume","prompt":"confirm","wait":5}`), nil)
339 if err != nil || result.IsError {
340 t.Fatalf("resume = (%#v, %v)", result, err)
341 }
342 response, ok := result.Details.(subagentResumeResponse)
343 if !ok || response.Wait == nil || response.Wait.TimedOut || response.Delivery != "queued" {
344 t.Fatalf("wait outcome = %#v", result.Details)
345 }
346 if response.Wait.Status != string(subagents.ResidentCompleted) || response.Wait.Summary != "answer for confirm" {
347 t.Fatalf("wait outcome = %#v", response.Wait)
348 }
349 if response.Action != "resumed" || response.Agent.ID != "waited-resume" {
350 t.Fatalf("response = %#v", response)
351 }
352}
353
354// An expired resume wait reports the timeout and leaves the child active.
355func TestResidentResumeWaitExpiryLeavesChildActive(t *testing.T) {
356 started := make(chan struct{}, 1)
357 release := make(chan struct{})
358 var releaseOnce sync.Once
359 releaseBlocker := func() { releaseOnce.Do(func() { close(release) }) }
360 manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
361 return subagents.ResidentTurnRunner(func(ctx context.Context, _ string) error {
362 started <- struct{}{}
363 select {
364 case <-release:
365 return nil
366 case <-ctx.Done():
367 return ctx.Err()
368 }
369 }), nil
370 })
371 t.Cleanup(func() {
372 releaseBlocker()
373 _ = manager.Close(context.Background())
374 })
375 if _, err := manager.Spawn(context.Background(), subagents.ResidentChildSpec{ID: "blocked-resume", InitialTurnID: "initial-turn", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}, "blocker"); err != nil {
376 t.Fatal(err)
377 }
378 select {
379 case <-started:
380 case <-time.After(5 * time.Second):
381 t.Fatal("blocking turn did not start")
382 }
383 resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
384 result, err := resume.Execute(context.Background(), json.RawMessage(`{"agent_id":"blocked-resume","prompt":"follow up","mode":"queue","wait":1}`), nil)
385 if err != nil || result.IsError {
386 t.Fatalf("resume = (%#v, %v)", result, err)
387 }
388 response, ok := result.Details.(subagentResumeResponse)
389 if !ok || response.Wait == nil || !response.Wait.TimedOut || response.Wait.Seconds != 1 {
390 t.Fatalf("wait outcome = %#v", result.Details)
391 }
392 if state, found := manager.State("blocked-resume"); !found || state != subagents.ResidentRunning {
393 t.Fatalf("child state = %q (found=%t), want running", state, found)
394 }
395 releaseBlocker()
396}
397
398// A cancelled parent context wins over an expiring wait: the tool returns the
399// host context error rather than a timeout result.
400func TestResidentResumeWaitCancellationBeatsTimeout(t *testing.T) {
401 release := make(chan struct{})
402 var releaseOnce sync.Once
403 releaseBlocker := func() { releaseOnce.Do(func() { close(release) }) }
404 var calls atomic.Int32
405 followUpStarted := make(chan struct{}, 1)
406 manager := subagents.NewResidentManager(t.TempDir(), func(subagents.ResidentChildSpec, *subagents.ResidentJournal) (subagents.ResidentRuntime, error) {
407 return subagents.ResidentTurnRunner(func(ctx context.Context, _ string) error {
408 if calls.Add(1) == 1 {
409 return nil
410 }
411 followUpStarted <- struct{}{}
412 select {
413 case <-release:
414 return nil
415 case <-ctx.Done():
416 return ctx.Err()
417 }
418 }), nil
419 })
420 t.Cleanup(func() {
421 releaseBlocker()
422 _ = manager.Close(context.Background())
423 })
424 initial, cancelInitial := manager.WatchCompletion("cancel-resume", "initial-turn")
425 defer cancelInitial()
426 if _, err := manager.Spawn(context.Background(), subagents.ResidentChildSpec{ID: "cancel-resume", InitialTurnID: "initial-turn", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}, "start"); err != nil {
427 t.Fatal(err)
428 }
429 select {
430 case <-initial:
431 case <-time.After(5 * time.Second):
432 t.Fatal("initial turn did not complete")
433 }
434
435 resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
436 ctx, cancel := context.WithCancel(context.Background())
437 defer cancel()
438 type outcome struct {
439 result core.ToolResult
440 err error
441 }
442 done := make(chan outcome, 1)
443 go func() {
444 result, err := resume.Execute(ctx, json.RawMessage(`{"agent_id":"cancel-resume","prompt":"follow up","wait":300}`), nil)
445 done <- outcome{result: result, err: err}
446 }()
447 select {
448 case <-followUpStarted:
449 case <-time.After(5 * time.Second):
450 t.Fatal("follow-up turn did not start")
451 }
452 cancel()
453 select {
454 case got := <-done:
455 if !errors.Is(got.err, context.Canceled) {
456 t.Fatalf("Execute error = %v, want context.Canceled (result=%#v)", got.err, got.result)
457 }
458 if got.result.IsError || len(got.result.Content) != 0 {
459 t.Fatalf("result = %#v, want an empty non-error result on host cancellation", got.result)
460 }
461 case <-time.After(5 * time.Second):
462 t.Fatal("resume did not return after cancellation")
463 }
464}
465
466func TestResidentResumeRejectsWaitOutsideBounds(t *testing.T) {
467 manager := subagents.NewResidentManager(t.TempDir(), nil)
468 t.Cleanup(func() { _ = manager.Close(context.Background()) })
469 resume := &SubagentResumeTool{ResidentManager: manager, Enabled: func() bool { return true }}
470 for _, raw := range []string{
471 `{"agent_id":"child","prompt":"x","wait":0}`,
472 `{"agent_id":"child","prompt":"x","wait":301}`,
473 } {
474 result, err := resume.Execute(context.Background(), json.RawMessage(raw), nil)
475 if err != nil || !result.IsError || !strings.Contains(toolResultText(t, result), "wait must be between 1 and 300 seconds") {
476 t.Fatalf("Execute(%s) = (%#v, %v)", raw, result, err)
477 }
478 }
479}
480
481func toolResultText(t *testing.T, result core.ToolResult) string {
482 t.Helper()
483 if len(result.Content) != 1 {
484 t.Fatalf("content = %#v, want one text block", result.Content)
485 }
486 block, ok := result.Content[0].(provider.TextBlock)
487 if !ok {
488 t.Fatalf("content = %#v, want text block", result.Content)
489 }
490 return block.Text
491}
492
493func TestPublicResidentStatusOmitsBudget(t *testing.T) {
494 entry := publicResidentStatus(subagents.ResidentSnapshot{
495 ID: "resident", State: subagents.ResidentIdle, Provider: "openai", Model: "test",
496 })
497 encoded, err := json.Marshal(entry)
498 if err != nil {
499 t.Fatal(err)
500 }
501 if strings.Contains(string(encoded), "budget") {
502 t.Fatalf("status entry carries budget = %s", encoded)
503 }
504}
505
506func TestFindResidentStatusSnapshotRejectsAmbiguousPrefix(t *testing.T) {
507 _, ok := findResidentStatusSnapshot([]subagents.ResidentSnapshot{{ID: "resident-abcd"}, {ID: "resident-abef"}}, "resident-ab")
508 if ok {
509 t.Fatal("ambiguous resident prefix resolved")
510 }
511}