Killer-Skills Review
Decision support comes first. Repository text comes second.
This page remains useful for teams, but Killer-Skills treats it as reference material instead of a primary organic landing page.
适用场景: Ideal for AI agents that need adding related views to a resource type. 本地化技能摘要: Terminal UI AWS Resource Manager — browse, inspect, and manage 60+ AWS resource types # Adding Related Views to a Resource Type Two agents, two tracks. It covers aws, aws-cli, bubbletea workflows. This AI agent skill supports Claude Code, Cursor, and Windsurf workflows.
核心价值
推荐说明: a9s-add-related-view helps agents adding related views to a resource type. Terminal UI AWS Resource Manager — browse, inspect, and manage 60+ AWS resource types # Adding Related Views to a Resource Type Two
适用 Agent 类型
适用场景: Ideal for AI agents that need adding related views to a resource type.
↓ 赋予的主要能力 · a9s-add-related-view
! 使用限制与门槛
- 限制说明: Infrastructure must be in place first. These must exist before any
- 限制说明: If these don't exist, STOP. The infrastructure must land first.
- 限制说明: Architect Must Provide
Why this page is reference-only
- - Current locale does not satisfy the locale-governance contract.
Source Boundary
The section below is imported from the upstream repository and should be treated as secondary evidence. Use the Killer-Skills review above as the primary layer for fit, risk, and installation decisions.
先决定动作,再继续看上游仓库材料
Killer-Skills 的主价值不应该停在“帮你打开仓库说明”,而是先帮你判断这项技能是否值得安装、是否应该回到可信集合复核,以及是否已经进入工作流落地阶段。
Browser Sandbox Environment
⚡️ Ready to unleash?
Experience this Agent in a zero-setup browser environment powered by WebContainers. No installation required.
常见问题与安装步骤
以下问题与步骤与页面结构化数据保持一致,便于搜索引擎理解页面内容。
? FAQ
a9s-add-related-view 是什么?
适用场景: Ideal for AI agents that need adding related views to a resource type. 本地化技能摘要: Terminal UI AWS Resource Manager — browse, inspect, and manage 60+ AWS resource types # Adding Related Views to a Resource Type Two agents, two tracks. It covers aws, aws-cli, bubbletea workflows. This AI agent skill supports Claude Code, Cursor, and Windsurf workflows.
如何安装 a9s-add-related-view?
运行命令:npx killer-skills add k2m30/a9s/a9s-add-related-view。支持 Cursor、Windsurf、VS Code、Claude Code 等 19+ IDE/Agent。
a9s-add-related-view 适用于哪些场景?
典型场景包括:适用任务: Applying Adding Related Views to a Resource Type、适用任务: Applying Two agents, two tracks. The architect scopes both tasks using the per-resource、适用任务: Applying research docs in docs/design/related-resources/{shortname}.md. Coder and QA。
a9s-add-related-view 支持哪些 IDE 或 Agent?
该技能兼容 Cursor, Windsurf, VS Code, Trae, Claude Code, OpenClaw, Aider, Codex, OpenCode, Goose, Cline, Roo Code, Kiro, Augment Code, Continue, GitHub Copilot, Sourcegraph Cody, and Amazon Q Developer。可使用 Killer-Skills CLI 一条命令通用安装。
a9s-add-related-view 有哪些限制?
限制说明: Infrastructure must be in place first. These must exist before any;限制说明: If these don't exist, STOP. The infrastructure must land first.;限制说明: Architect Must Provide。
↓ 安装步骤
-
1. 打开终端
在你的项目目录中打开终端或命令行。
-
2. 执行安装命令
运行:npx killer-skills add k2m30/a9s/a9s-add-related-view。CLI 会自动识别 IDE 或 AI Agent 并完成配置。
-
3. 开始使用技能
a9s-add-related-view 已启用,可立即在当前项目中调用。
! 参考页模式
此页面仍可作为安装与查阅参考,但 Killer-Skills 不再把它视为主要可索引落地页。请优先阅读上方评审结论,再决定是否继续查看上游仓库说明。
Upstream Repository Material
The section below is imported from the upstream repository and should be treated as secondary evidence. Use the Killer-Skills review above as the primary layer for fit, risk, and installation decisions.
a9s-add-related-view
安装 a9s-add-related-view,这是一款面向AI agent workflows and automation的 AI Agent Skill。查看评审结论、使用场景与安装路径。
Adding Related Views to a Resource Type
Two agents, two tracks. The architect scopes both tasks using the per-resource
research docs in docs/design/related-resources/{shortname}.md. Coder and QA
can run in parallel (pattern is rigid).
Prerequisites
Infrastructure must be in place first. These must exist before any per-resource related views can be added:
internal/resource/related.go-- types, registries (RelatedDef,NavigableField), helper constructorsRelatedCheckResultMsgandRelatedNavigateMsgininternal/tui/messages/messages.goToggleRelatedbinding ininternal/tui/keys/keys.go- Two-column detail view in
internal/tui/views/detail.go(field-list model with embedded rightColumnModel) - Handler code in
internal/tui/app.go(main Update switch) andinternal/tui/app_related.goforRelatedCheckResultMsgandRelatedNavigateMsg
If these don't exist, STOP. The infrastructure must land first.
See docs/design/related-views-architecture.md Phases 1-8.
Architect Must Provide
You MUST have a scoped task from the architect with:
- Source type ShortName (e.g., "ec2")
- Left column (navigable fields):
- For each navigable field: FieldPath, TargetType
- Right column (related types):
- For each related type:
- Target ShortName (e.g., "sg", "vpc")
- DisplayName override (if any)
- For cache-based checks: which cache key to look up, how to match
- For field-based checks: which Fields key to read
- For each related type:
- Exact files to create/modify with append points
If you don't have this, STOP. Reply with REJECTED and ask for architect scope.
Agent Ownership
| Steps | Owner | Writes to |
|---|---|---|
| 1-7 (implementation) | a9s-coder | internal/, cmd/ |
| 8-12 (tests) | a9s-qa | tests/unit/ |
Coder MUST NOT write test files. QA MUST NOT write production code.
Relationship Patterns
Pattern F: Forward / Field-Based (cheap, count shown)
IDs are already in the source resource's Fields or RawStruct. No external API call needed. The checker reads from Fields or RawStruct directly.
go1// Example: EC2 -> EBS volumes (read from RawStruct block device mappings) 2func checkEC2EBS(_ context.Context, _ interface{}, res resource.Resource, _ resource.ResourceCache) resource.RelatedCheckResult { 3 ids := ec2VolumeIDs(res) // reads from res.RawStruct 4 if len(ids) == 0 { 5 return resource.RelatedCheckResult{TargetType: "ebs", Count: 0} 6 } 7 ordered := make([]string, 0, len(ids)) 8 for id := range ids { 9 ordered = append(ordered, id) 10 } 11 sort.Strings(ordered) 12 return relatedResult("ebs", ordered) 13}
Pattern C: Cache-Based (reads from already-loaded resource lists)
Looks up related resources in the ResourceCache. Falls back to a live API call only when the cache doesn't contain the target type. All cache-based checkers follow this helper pattern:
go1func check{Source}{Target}(ctx context.Context, clients interface{}, res resource.Resource, cache resource.ResourceCache) resource.RelatedCheckResult { 2 // 1. Extract identity from res (ID, relevant fields, or RawStruct) 3 sourceID, _ := extractSourceIdentity(res) 4 if sourceID == "" { 5 return resource.RelatedCheckResult{TargetType: "{target}", Count: 0} 6 } 7 8 // 2. Load target list from cache or live fetch 9 targetList, truncated, err := {source}RelatedResources(ctx, clients, cache, "{target}") 10 if err != nil { 11 return resource.RelatedCheckResult{TargetType: "{target}", Count: -1, Err: err} 12 } 13 if targetList == nil { 14 // Target fetcher not registered / nothing loaded yet — honest lower 15 // bound is zero, NOT an error. See "Count: -1 vs ApproximateZero" below. 16 return resource.ApproximateZero("{target}") 17 } 18 19 // 3. Match against source 20 var ids []string 21 for _, targetRes := range targetList { 22 // match by RawStruct fields first, fall back to resource.Fields 23 if matchesSource(targetRes, sourceID) { 24 ids = append(ids, targetRes.ID) 25 } 26 } 27 // Truncation guard: partial page with 0 matches → honest lower bound, not an error. 28 if len(ids) == 0 && truncated { 29 return resource.ApproximateZero("{target}") 30 } 31 return relatedResult("{target}", ids) 32}
The relatedResult helper (defined in ec2_related.go, copy-paste for other
source types) deduplicates and sorts IDs:
go1func relatedResult(target string, ids []string) resource.RelatedCheckResult { 2 if len(ids) == 0 { 3 return resource.RelatedCheckResult{TargetType: target, Count: 0} 4 } 5 // deduplicate and sort ids ... 6 return resource.RelatedCheckResult{ 7 TargetType: target, 8 Count: len(uniq), 9 ResourceIDs: uniq, 10 } 11}
Count semantics:
Count: 0-- confirmed none (exhaustive scan produced no matches)Count: N(N > 0) -- confirmed N found,ResourceIDspopulatedCount: -1-- error state. Something went wrong: AWS API returned an error, required client is nil,assertStructfailed, or we lack the identity needed to run the query. The UI renders this as"?"and it contributes to the per-resource truncation bucket.resource.ApproximateZero("{target}")-- honest lower bound of zero. Not an error. Returned when the target list comes back nil (fetcher not registered, nothing loaded yet) or when a truncated target page produced zero matches so far. The UI renders this as"0+".
Return-value classifier for a checker -- load-bearing
A related checker must choose one of four return values per code path. Getting this wrong misleads the UI: Count: -1 renders as "?" and feeds the truncation bucket; ApproximateZero renders as "0+"; Count: 0 renders as plain "0". The operator reads those three differently. Classify every branch:
-
Error path -- AWS call returned an error, required client is nil, an
assertStructfailed, the source resource lacks the identity needed to form the query, or an intermediate helper returned a sentinel meaning "unknown, could not determine". Returnresource.RelatedCheckResult{TargetType: "{target}", Count: -1, Err: err}(attachErrwhen a realerrorvalue is in hand). If an intermediate helper's documented contract is "nil return = error distinct from empty", propagate its nil asCount: -1; do NOT collapse it intoApproximateZero. -
Complete-answer API -- the checker makes a single bounded AWS call (not cache-backed, not paginated) that returns an exhaustive answer.
out == nil && err == nil(or an empty output slice with no error) means the AWS service reported zero authoritatively. ReturnCount: 0. This is NOTApproximateZero-- no lower bound is involved because no pagination cursor remains. -
Nil target list from the cache-backed helper -- the checker uses the standard
{source}RelatedResources(ctx, clients, cache, "{target}")(or equivalentFetchRelatedTargetwrapper) and the returned list isnilbecause the target fetcher is not registered or nothing has been loaded yet. Returnresource.ApproximateZero("{target}"). Zero registered targets is an honest lower bound of zero, not an error. -
Truncated target page with zero matches so far -- the cache-backed helper returned a partial page (
truncated == true) and this checker's match loop produced no IDs. Returnresource.ApproximateZero("{target}"). Later pages may produce matches; the current view is a lower bound.
Decision order when reviewing a new or rewritten checker: for each return statement, ask (in order) -- Is an AWS call or helper erroring? Is this a one-shot exhaustive API with out == nil && err == nil? Is this the if targetList == nil branch from a cache-backed helper? Is this the truncated-and-no-matches branch? The first "yes" determines the return. Returning Count: -1 on a nil-list-from-cache or truncated-empty branch is the regression this section exists to prevent.
Pattern N: Naming-Convention (reverse lookup by name pattern)
The target resource's ID or name follows a predictable naming convention that embeds the source resource's name. Search the target cache for matches.
go1// Example: SFN → logs (log group name is /aws/vendedlogs/states/{sfn-name}) 2func checkSFNLogs(ctx context.Context, clients any, res resource.Resource, cache resource.ResourceCache) resource.RelatedCheckResult { 3 sfnName := res.ID 4 if sfnName == "" { 5 return resource.RelatedCheckResult{TargetType: "logs", Count: 0} 6 } 7 expectedPrefix := "/aws/vendedlogs/states/" + sfnName 8 9 logsList, truncated, err := sfnRelatedResources(ctx, clients, cache, "logs") 10 // ... standard cache lookup, match by strings.HasPrefix(logRes.ID, expectedPrefix) 11}
Known naming conventions:
- Lambda → logs:
/aws/lambda/{function-name} - CodeBuild → logs:
/aws/codebuild/{project-name} - SFN → logs:
/aws/vendedlogs/states/{state-machine-name} - EKS → logs:
/aws/eks/{cluster-name}/...
Pattern D: Dimension-Based (reverse lookup by alarm dimensions)
Search the alarm cache for alarms whose Dimensions[] contain a matching
dimension name/value. The source resource's ARN or name is the dimension value.
go1// Example: SFN → alarm (alarm dimension StateMachineArn matches SFN ARN) 2func checkSFNAlarm(ctx context.Context, clients any, res resource.Resource, cache resource.ResourceCache) resource.RelatedCheckResult { 3 sfnARN := res.Fields["arn"] 4 if sfnARN == "" { 5 return resource.RelatedCheckResult{TargetType: "alarm", Count: -1} 6 } 7 alarmList, truncated, err := sfnRelatedResources(ctx, clients, cache, "alarm") 8 // ... iterate alarms, assert MetricAlarm, check Dimensions for StateMachineArn == sfnARN 9}
When Checker: nil Is Acceptable
A nil checker (unknown count, shows "?" in UI) is ONLY acceptable when ALL of the following are true:
- No forward fields: The source RawStruct has no fields referencing the target
- No reverse fields: No cached resource type has fields referencing this source
- No naming convention: The target doesn't follow a name pattern embedding the source name
- No dimension match: No alarm dimensions reference this source's ARN or name
- The relationship requires a separate API call not available from any cached data
Before marking a checker as nil, the architect MUST verify all five conditions.
Checking the research doc (docs/design/related-resources/{shortname}.md) is
mandatory — it lists viable lookup strategies for each relationship.
Common mistakes:
- Marking alarm as nil when alarms have
Dimensions[]referencing the source ARN - Marking logs as nil when a
/aws/{service}/{name}log group convention exists - Marking a relationship as nil when OTHER cached resources have fields pointing back
(e.g., SNS→alarm: alarm cache has
AlarmActions[]containing topic ARNs)
CODER STEPS (1-7) -- a9s-coder agent only
1. Registration: add to internal/aws/{source}.go (or {source}_related.go)
IMPORTANT: Module path is github.com/k2m30/a9s/v3/... (the /v3 suffix is required).
Both RegisterRelated and RegisterNavigableFields calls belong in the
same init() as the resource type registration. For large resources like EC2
the related checker functions live in a separate {source}_related.go file
for readability, but the RegisterRelated calls stay in the main init().
go1func init() { 2 // ... existing RegisterType / RegisterFetcher calls ... 3 4 // --- Right column: related resource definitions --- 5 resource.RegisterRelated("{source}", []resource.RelatedDef{ 6 // NeedsTargetCache: true for Pattern C (cache-based) checkers — triggers pre-fetch before checker runs. 7 // Omit (false) for Pattern A/B checkers that call AWS directly. 8 {TargetType: "{target1}", DisplayName: "{Display Name 1}", Checker: check{Source}{Target1}, NeedsTargetCache: true}, 9 {TargetType: "{target2}", DisplayName: "{Display Name 2}", Checker: check{Source}{Target2}, NeedsTargetCache: true}, 10 // Checker may be nil for stubs (shows as unknown count): 11 {TargetType: "{target3}", DisplayName: "{Display Name 3}", Checker: nil}, 12 }) 13 14 // --- Left column: navigable fields --- 15 resource.RegisterNavigableFields("{source}", []resource.NavigableField{ 16 {FieldPath: "{FieldName}", TargetType: "{target}"}, 17 {FieldPath: "{Section.FieldName}", TargetType: "{target}"}, 18 // ... one entry per navigable field in the detail view 19 }) 20}
RelatedDef fields:
TargetType string-- target resource short name (e.g., "tg", "alarm")DisplayName string-- right-column row label (e.g., "Target Groups")Checker RelatedChecker-- async checker function (nil for stubs)NeedsTargetCache bool-- settruefor Pattern C (cache-based) checkers; triggers coordinated pre-fetch before the checker runs. Omitting this on a cache-based checker causes cold-cache misses to returnCount: -1instead of the real count.
NavigableField fields:
FieldPath string-- matches a label rendered in the detail view (e.g., "VpcId")TargetType string-- resource short name to navigate to
2. Checker functions: internal/aws/{source}_related.go (NEW FILE)
The RelatedChecker type signature is:
go1type RelatedChecker func(ctx context.Context, clients interface{}, res resource.Resource, cache resource.ResourceCache) resource.RelatedCheckResult
Note: no error return -- errors are embedded in RelatedCheckResult.Err.
go1package aws 2 3import ( 4 "context" 5 "sort" 6 7 // ... SDK type imports as needed ... 8 9 "github.com/k2m30/a9s/v3/internal/resource" 10) 11 12// check{Source}{Target1} checks the cache for {target1} resources related to this {source}. 13func check{Source}{Target1}(ctx context.Context, clients interface{}, res resource.Resource, cache resource.ResourceCache) resource.RelatedCheckResult { 14 sourceID := res.ID 15 if sourceID == "" { 16 return resource.RelatedCheckResult{TargetType: "{target1}", Count: 0} 17 } 18 19 targetList, truncated, err := {source}RelatedResources(ctx, clients, cache, "{target1}") 20 if err != nil { 21 return resource.RelatedCheckResult{TargetType: "{target1}", Count: -1, Err: err} 22 } 23 if targetList == nil { 24 // Honest zero — fetcher not registered or nothing loaded yet. 25 return resource.ApproximateZero("{target1}") 26 } 27 28 var ids []string 29 for _, r := range targetList { 30 // prefer RawStruct for accuracy, fall back to Fields 31 if r.Fields["{source_id_field}"] == sourceID { 32 ids = append(ids, r.ID) 33 } 34 } 35 // Truncation guard: partial page with 0 matches → honest lower bound. 36 if len(ids) == 0 && truncated { 37 return resource.ApproximateZero("{target1}") 38 } 39 return relatedResult("{target1}", ids) 40} 41 42// check{Source}{Target2} checks the cache for {target2} resources related to this {source}. 43func check{Source}{Target2}(ctx context.Context, clients interface{}, res resource.Resource, cache resource.ResourceCache) resource.RelatedCheckResult { 44 // ... similar pattern ... 45} 46 47// {source}RelatedResources returns the resource list for target from cache or 48// fetches the first page via the registered paginated fetcher. 49// Returns (resources, isTruncated, error). 50// isTruncated=true means the list is partial; callers MUST return Count=-1 51// when 0 matches are found in a truncated list. 52func {source}RelatedResources(ctx context.Context, clients interface{}, cache resource.ResourceCache, target string) ([]resource.Resource, bool, error) { 53 resources, isTruncated, err := FetchRelatedTarget(ctx, clients, cache, target) 54 // When AWS clients are not initialized (nil or wrong type), registered fetchers 55 // return "AWS clients not initialized". Treat as graceful no-op (Count=-1, no error). 56 if err != nil { 57 if _, ok := clients.(*ServiceClients); !ok { 58 return nil, false, nil 59 } 60 } 61 return resources, isTruncated, err 62} 63 64// Do NOT define a {source}RelatedResult function. Call the shared package-level 65// relatedResult(target, ids) from ec2_related.go — it lives in the same package 66// and handles deduplication and sorting for all resource types.
RelatedCheckResult fields:
TargetType string-- echoed fromRelatedDef.TargetTypeCount int-- -1 = unknown; 0 = confirmed none; N > 0 = confirmed NResourceIDs []string-- IDs of found related resources (empty when Count <= 0)Err error-- non-nil on error
There is no Available bool field.
3. Interfaces: internal/aws/<service>_interfaces.go (APPEND if needed)
Only needed if the checker's live-fetch fallback calls an API not already covered by existing interfaces. Cache-only checkers that never call live APIs do not need new interfaces.
go1// Only add if not already present: 2type {TypeName}{APICall}API interface { 3 {APICall}(ctx context.Context, params *{service}.{APICall}Input, optFns ...func(*{service}.Options)) (*{service}.{APICall}Output, error) 4}
4. Demo overrides
Register a demo checker so the related panel shows realistic data in demo mode.
Hybrid fixture pattern (014-demo-transport-mock). Demo mode has two layers: the legacy HTTP transport (internal/demo/transport.go + handlers.go) is the base for all services, and per-service typed fakes (internal/demo/fakes/<service>.go) override individual services. Currently only EC2 uses a typed fake.
- Preferred (migrated services): add fixture data to
internal/demo/fixtures/<service>.goand extend the matching fake ininternal/demo/fakes/<service>.go. - Legacy (non-migrated services): add fixture data to the matching
internal/demo/fixtures_*.gocategory file and (if needed) extend handlers ininternal/demo/handlers.go.
When adding a new resource type, match the service's current layer. Do not mix layers for the same service.
Demo fixture support for related views
No separate demo registry is needed. Related checkers run against the typed fakes automatically. Ensure the target resource type's fixtures contain IDs that match what the source resource's fields reference. For example, if EC2 instances reference vpc-prod-main in their VpcId field, the VPC fake's fixtures must include a VPC with that ID.
Add fixture data to internal/demo/fixtures/<service>.go for the target resource type. The related checker will find it via the standard prefetch + cache path.
Never amend tests if fixtures do not have related IDs/fields. Fix fixtures, not tests.
Use the resource short name in PascalCase as {SourceCamel} and {TargetCamel}:
ec2 → EC2, rds → RDS, s3 → S3, tg → TG, ebs → EBS, etc.
Add a numeric suffix only when a source has multiple related IDs of the same target type.
5. Verify parent resource Fields
Critical for left-column navigable fields. Verify that the source
resource's regular fetcher populates the Fields keys that the
NavigableField entries reference.
For example, if a NavigableField has FieldPath: "VpcId", verify
that internal/aws/{source}.go populates a field with key "VpcId" or that
the field appears in the detail view from RawStruct reflection.
If a required field is missing from Fields, add it to the regular fetcher.
6. Verify navigable field paths match detail view output
The FieldPath in NavigableField must match the actual key labels
rendered in the detail view. A mismatch silently leaves the field non-highlighted
across the entire resource type.
Automated check (required): Run the test added in QA step 9
(TestNavigableFields_{Source}_FieldPathsResolve) which verifies each registered
FieldPath resolves to a non-empty value against the resource's demo fixture:
bash1go test ./tests/unit/ -run "TestNavigableFields_{Source}_FieldPathsResolve" -v -count=1
Manual fallback: If no demo fixture exists yet, check .a9s/views_reference.yaml
for the source resource — all field paths that appear in the detail view are
listed there. Verify each FieldPath matches a key in that file (case-sensitive).
For nested fields like SecurityGroups.GroupId, the detail view renders
these as indented sub-fields. The FieldPath must match the leaf label
that appears in the rendered output.
7. Post-implementation verification
bash1make test 2make lint 3make gofix 4make build
QA STEPS (8-12) -- a9s-qa agent only
8. Mocks: tests/unit/mocks_test.go (APPEND if needed)
Only needed if the checker's live-fetch fallback introduces NEW interfaces not already present. Cache-only checkers that never call live APIs do not need new mocks.
go1// mock{TypeName}By{Filter}Client implements awsclient.{InterfaceName} for testing. 2type mock{TypeName}By{Filter}Client struct { 3 output *{service}.{APICall}Output 4 err error 5} 6 7func (m *mock{TypeName}By{Filter}Client) {APICall}( 8 ctx context.Context, 9 params *{service}.{APICall}Input, 10 optFns ...func(*{service}.Options), 11) (*{service}.{APICall}Output, error) { 12 return m.output, m.err 13}
9. Related checker tests: tests/unit/aws_{source}_related_test.go (NEW FILE)
Write tests covering each checker and navigable field registration.
Checkers receive a resource.ResourceCache -- populate it with test data
to simulate the cache-hit path. Test the cache-miss path by passing an
empty or nil cache.
go1package unit_test 2 3import ( 4 "context" 5 "testing" 6 7 // ... SDK type imports as needed ... 8 9 "github.com/k2m30/a9s/v3/internal/demo" 10 "github.com/k2m30/a9s/v3/internal/fieldpath" 11 "github.com/k2m30/a9s/v3/internal/resource" 12) 13 14func {source}CheckerByTarget(t *testing.T, target string) resource.RelatedChecker { 15 t.Helper() 16 for _, def := range resource.GetRelated("{source}") { 17 if def.TargetType == target { 18 if def.Checker == nil { 19 t.Fatalf("{source} related checker for %s is nil", target) 20 } 21 return def.Checker 22 } 23 } 24 t.Fatalf("{source} related checker for %s not found", target) 25 return nil 26} 27 28// --- Navigable Field Registration Tests --- 29 30func TestNavigableFields_{Source}_Registered(t *testing.T) { 31 fields := resource.GetNavigableFields("{source}") 32 if len(fields) == 0 { 33 t.Fatal("no navigable fields registered for {source}") 34 } 35 36 expected := map[string]string{ 37 "{FieldPath1}": "{target1}", 38 "{FieldPath2}": "{target2}", 39 } 40 for path, targetType := range expected { 41 nav := resource.IsFieldNavigable("{source}", path) 42 if nav == nil { 43 t.Errorf("expected navigable field %q not found", path) 44 continue 45 } 46 if nav.TargetType != targetType { 47 t.Errorf("field %q: TargetType = %q, want %q", path, nav.TargetType, targetType) 48 } 49 } 50} 51 52// TestNavigableFields_{Source}_FieldPathsResolve verifies that each registered 53// NavigableField.FieldPath resolves to a non-empty value against the demo fixture. 54// A mismatch here means the field will silently never be highlighted in the detail view. 55func TestNavigableFields_{Source}_FieldPathsResolve(t *testing.T) { 56 // Get demo resource for this source type (must be populated by the demo fixture) 57 resources, ok := demo.GetResources("{source}") 58 if !ok { 59 t.Skip("no demo fixture registered for {source}") 60 } 61 if len(resources) == 0 { 62 t.Skip("demo fixture returned no resources for {source}") 63 } 64 r := resources[0] 65 66 fields := resource.GetNavigableFields("{source}") 67 if len(fields) == 0 { 68 t.Fatal("no navigable fields registered for {source}") 69 } 70 71 for _, nav := range fields { 72 items := fieldpath.ExtractFieldList(r.RawStruct, []string{nav.FieldPath}, r.Fields, nil) 73 found := false 74 for _, item := range items { 75 if item.Value != "" && item.Value != "-" { 76 found = true 77 break 78 } 79 } 80 if !found { 81 t.Errorf("NavigableField.FieldPath %q resolved to empty/missing value in demo fixture — check FieldPath spelling or add field to fetcher", nav.FieldPath) 82 } 83 } 84} 85 86// --- Checker Tests --- 87 88func TestRelated_{Source}_{Target}_Found(t *testing.T) { 89 // Build a fake target resource that should match the source 90 fakeTarget := resource.Resource{ 91 ID: "target-id-1", 92 Fields: map[string]string{ 93 "{source_id_field}": "source-id-1", 94 }, 95 } 96 cache := resource.ResourceCache{ 97 "{target}": resource.ResourceCacheEntry{Resources: []resource.Resource{fakeTarget}}, 98 } 99 source := resource.Resource{ID: "source-id-1"} 100 101 checker := {source}CheckerByTarget(t, "{target}") 102 result := checker(context.Background(), nil, source, cache) 103 104 if result.Count != 1 { 105 t.Errorf("Count = %d, want 1", result.Count) 106 } 107 if len(result.ResourceIDs) != 1 || result.ResourceIDs[0] != "target-id-1" { 108 t.Errorf("ResourceIDs = %v, want [target-id-1]", result.ResourceIDs) 109 } 110 if result.Err != nil { 111 t.Errorf("unexpected error: %v", result.Err) 112 } 113} 114 115func TestRelated_{Source}_{Target}_NotFound(t *testing.T) { 116 fakeTarget := resource.Resource{ 117 ID: "target-id-2", 118 Fields: map[string]string{ 119 "{source_id_field}": "other-source-id", 120 }, 121 } 122 cache := resource.ResourceCache{ 123 "{target}": resource.ResourceCacheEntry{Resources: []resource.Resource{fakeTarget}}, 124 } 125 source := resource.Resource{ID: "source-id-1"} 126 127 checker := {source}CheckerByTarget(t, "{target}") 128 result := checker(context.Background(), nil, source, cache) 129 130 if result.Count != 0 { 131 t.Errorf("Count = %d, want 0", result.Count) 132 } 133 if len(result.ResourceIDs) != 0 { 134 t.Errorf("ResourceIDs = %v, want []", result.ResourceIDs) 135 } 136} 137 138func TestRelated_{Source}_{Target}_CacheMissNoClients(t *testing.T) { 139 // Empty cache + nil clients -> unknown (-1), no error 140 source := resource.Resource{ID: "source-id-1"} 141 checker := {source}CheckerByTarget(t, "{target}") 142 result := checker(context.Background(), nil, source, resource.ResourceCache{}) 143 144 if result.Count != -1 { 145 t.Errorf("Count = %d, want -1 (unknown)", result.Count) 146 } 147} 148 149func TestRelated_{Source}_{Target}_EmptySourceID(t *testing.T) { 150 source := resource.Resource{ID: ""} 151 checker := {source}CheckerByTarget(t, "{target}") 152 result := checker(context.Background(), nil, source, resource.ResourceCache{}) 153 154 if result.Count != 0 { 155 t.Errorf("Count = %d, want 0 for empty source ID", result.Count) 156 } 157}
10. Cold-cache checker tests (MANDATORY for every registered source type)
Add to the same tests/unit/aws_{source}_related_test.go file. Tests drive the real checker through the typed fakes via the cold-cache harness — no demo registry needed:
go1func TestRelated_{Source}_ColdCacheChecker(t *testing.T) { 2 m := newDemoColdCacheApp(t) 3 // Size, inject clients, navigate to source, fetch, open detail 4 // Assert related check produces non-empty results via the real checker path 5}
11. Registry tests: tests/unit/related_registry_test.go (APPEND)
Verify the registration was successful:
go1func TestRelated_{Source}_Registered(t *testing.T) { 2 defs := resource.GetRelated("{source}") 3 if len(defs) == 0 { 4 t.Fatal("no related defs registered for {source}") 5 } 6 7 // Verify expected target types are present 8 expected := []string{"{target1}", "{target2}", "{target3}"} 9 for _, exp := range expected { 10 found := false 11 for _, def := range defs { 12 if def.TargetType == exp { 13 found = true 14 break 15 } 16 } 17 if !found { 18 t.Errorf("expected related def for target %q not found", exp) 19 } 20 } 21 22 // Verify non-stub checkers exist 23 for _, def := range defs { 24 if def.Checker == nil { 25 continue // stub entry, intentional 26 } 27 // Verify checker is callable (non-nil is sufficient for registration check) 28 } 29}
12. Post-test verification
bash1go test ./tests/unit/ -count=1 -timeout 120s -run "Related_{Source}" 2go test ./tests/unit/ -count=1 -timeout 120s -run "NavigableFields_{Source}" 3make test 4make lint 5make gofix
What You Do NOT Need to Change (per resource)
detail.go-- the two-column detail view renders from registries genericallyapp.go-- generic handlers dispatch to registrymessages.go-- generic message types carry stringskeys.go--rtoggle andTabswitching are genericapp_related.go--handleRelatedCheckStarted()andhandleRelatedNavigate()are generic
Research Reference
Each resource's related relationships are documented in:
docs/design/related-resources/{shortname}.md
These docs contain:
- Real-world use cases (why engineers need this relationship)
- Which other resource types reference or are referenced by this resource
- Which Fields/RawStruct paths to use for matching
Forward relationships and navigable field paths come from the resource's
own API response fields, documented in .a9s/views_reference.yaml.
Architect Handoff Format
When the architect scopes related views for resource X, the handoff uses:
## RELATED VIEWS: {Source Display Name} ({source_shortname})
### Left Column -- Navigable Fields:
| Field Path | Target Type | Notes |
|------------|-------------|-------|
| {FieldPath} | {target} | {optional notes} |
| {Section.Field} | {target} | {optional notes} |
### Right Column -- Related Definitions:
| Target | DisplayName | Match Strategy | Cache Key | Notes |
|--------|------------|----------------|-----------|-------|
| {target} | {Display Name} | field: {field_key} == sourceID | {cache_key} | {notes} |
| {target} | {Display Name} | rawstruct: {field_path} | {cache_key} | {notes} |
| {target} | {Display Name} | nil (stub) | n/a | |
### CODER TASK:
Files to create:
internal/aws/{source}_related.go -- checker functions + cache helper (reuse shared relatedResult and assertStruct from ec2_related.go — do NOT redefine)
Files to modify:
internal/aws/{source}.go -- append RegisterRelated + RegisterNavigableFields in init()
internal/demo/fixtures/<service>.go -- ensure target resource fixtures contain matching IDs
internal/aws/{service}_interfaces.go -- append new interfaces (only if live-fetch fallback needs them)
Append point: after last narrow interface, before the aggregate {Service}API
Context files (read-only):
internal/aws/{source}.go -- verify Fields keys exist
internal/aws/ec2_related.go -- canonical checker pattern
internal/resource/related.go -- type definitions
docs/design/related-resources/{source}.md -- relationship details
.a9s/views_reference.yaml -- verify field paths
### QA TASK:
Test files to create:
tests/unit/aws_{source}_related_test.go -- checker + navigable field + demo tests
Test files to modify:
tests/unit/mocks_test.go -- append mocks (only if live-fetch fallback needs new interfaces)
Append point: last mock in file
tests/unit/related_registry_test.go -- append registration tests
Append point: last TestRelated_ function
What to test:
- Navigable field registration: all expected fields registered with correct target types
- Checkers: found / not found / cache-miss-no-clients / empty-source-ID
- Cold-cache checker: drives real checker through typed fakes via newDemoColdCacheApp
- Registry: all expected defs registered
Context files (read-only):
internal/aws/{source}_related.go -- function signatures
internal/resource/related.go -- type definitions