Files
dingtalk-workspace-cli/cmd/fetch_mcp_metadata/main_test.go
玉澜andCursor 9d1c3c5c95 fix(ci): restore schema-compat and coverage after MCP pin retirement
Allow clearing interface_type and expanding constraint group members so
MCP-pin retirement and declare≡execute alias groups stay backward-compatible.
Close platform coverage gaps with TestCrossPlatformCoverage* and bump the
ContractFinal consistency count to 847.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 17:58:38 +08:00

613 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"math"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/syncdata"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageLoadRegistryInterfaceRefsCollectsIdentity(t *testing.T) {
var stderr bytes.Buffer
refs := loadRegistryInterfaceRefs(&stderr)
if len(refs) == 0 {
t.Fatal("loadRegistryInterfaceRefs() returned no collected commands")
}
got, ok := refs["calendar.list_calendars"]
if !ok {
t.Fatal("calendar.list_calendars missing from collected command identity")
}
if got["product_id"] != "calendar" || got["rpc_name"] != "list_calendars" {
t.Fatalf("calendar.list_calendars ref = %#v", got)
}
direct, err := collectedIdentityInterfaceRefs()
if err != nil {
t.Fatalf("collectedIdentityInterfaceRefs() error = %v", err)
}
if len(direct) == 0 || direct["calendar.list_calendars"]["rpc_name"] != "list_calendars" {
t.Fatalf("collectedIdentityInterfaceRefs() = %#v", direct["calendar.list_calendars"])
}
prevRegistry := registrySource
prevCollect := collectIdentitySpecs
t.Cleanup(func() {
registrySource = prevRegistry
collectIdentitySpecs = prevCollect
})
registrySource = func() (map[string]map[string]string, error) {
return nil, errors.New("collect boom")
}
stderr.Reset()
if got := loadRegistryInterfaceRefs(&stderr); len(got) != 0 || !strings.Contains(stderr.String(), "cannot collect command identity") {
t.Fatalf("loadRegistryInterfaceRefs error path = %#v stderr=%q", got, stderr.String())
}
collectIdentitySpecs = func(*cobra.Command) ([]cli.CommandSpec, cli.IdentityCollectionReport, error) {
return nil, cli.IdentityCollectionReport{}, errors.New("walk boom")
}
if _, err := collectedIdentityInterfaceRefs(); err == nil || !strings.Contains(err.Error(), "collect command identity") {
t.Fatalf("collectedIdentityInterfaceRefs wrap error = %v", err)
}
collectIdentitySpecs = func(*cobra.Command) ([]cli.CommandSpec, cli.IdentityCollectionReport, error) {
return []cli.CommandSpec{
{CanonicalPath: ""},
{CanonicalPath: "nodot"},
{CanonicalPath: "doc.create"},
}, cli.IdentityCollectionReport{}, nil
}
gotRefs, err := collectedIdentityInterfaceRefs()
if err != nil || len(gotRefs) != 1 || gotRefs["doc.create"]["rpc_name"] != "create" {
t.Fatalf("collectedIdentityInterfaceRefs skip = %#v err=%v", gotRefs, err)
}
}
func TestBuildCrossServerRefs(t *testing.T) {
registryMap := map[string]map[string]string{
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
"aitable.advperm_disable": {"product_id": "aitable", "rpc_name": "advperm_disable"},
"doc.copy_document": {"product_id": "doc", "rpc_name": "copy_document"},
}
prevTools := map[string]map[string]any{
// Fan-out: two canonicals share one live tool; insertion order must
// not affect the sorted result.
"aitable.advperm_enable": {
"interface_ref": map[string]any{"product_id": "aitable-helper", "rpc_name": "set_advanced_permission"},
},
"aitable.advperm_disable": {
"interface_ref": map[string]any{"product_id": "aitable-helper", "rpc_name": "set_advanced_permission"},
},
// Identity ref (live key == canonical) needs no cross entry.
"doc.copy_document": {
"interface_ref": map[string]any{"product_id": "doc", "rpc_name": "copy_document"},
},
// Not in the registry: must be ignored.
"ghost.tool": {
"interface_ref": map[string]any{"product_id": "ghost-helper", "rpc_name": "haunt"},
},
}
got := buildCrossServerRefs(prevTools, registryMap)
want := map[string][]string{
"aitable-helper.set_advanced_permission": {"aitable.advperm_disable", "aitable.advperm_enable"},
}
if len(got) != len(want) {
t.Fatalf("index = %#v, want %#v", got, want)
}
for k, v := range want {
if gv := got[k]; len(gv) != len(v) || gv[0] != v[0] || gv[1] != v[1] {
t.Fatalf("index[%q] = %v, want %v", k, gv, v)
}
}
}
func TestBuildCrossServerRefsSkipsMalformedRefs(t *testing.T) {
registryMap := map[string]map[string]string{
"a.x": {"product_id": "a", "rpc_name": "x"},
"a.y": {"product_id": "a", "rpc_name": "y"},
"a.z": {"product_id": "a", "rpc_name": "z"},
}
prevTools := map[string]map[string]any{
"a.x": {"interface_ref": "not-a-map"},
"a.y": {"interface_ref": map[string]any{"product_id": "", "rpc_name": "r"}},
"a.z": {"title": "no ref at all"},
}
if got := buildCrossServerRefs(prevTools, registryMap); len(got) != 0 {
t.Fatalf("index = %#v, want empty", got)
}
}
func TestRunRefreshesCrossServerTools(t *testing.T) {
registry := func() (map[string]map[string]string, error) {
return map[string]map[string]string{
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
"aitable.advperm_disable": {"product_id": "aitable", "rpc_name": "advperm_disable"},
}, nil
}
servers := []syncdata.ServerInfo{{ID: "aitable-helper", Endpoint: "https://helper.example"}}
lister := &fakeLister{
results: map[string]transport.ToolsListResult{
"https://helper.example": {Tools: []transport.ToolDescriptor{
{Name: "set_advanced_permission", Title: "live title", Description: "live desc"},
}},
},
}
stubDeps(t, "env-token", nil, servers, lister, registry)
output := filepath.Join(t.TempDir(), "snapshot.json")
prev := `{"tools":{
"aitable.advperm_enable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}},
"aitable.advperm_disable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}}
}}`
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
t.Fatal(err)
}
var stderr bytes.Buffer
if code := run([]string{"--output", output}, &stderr); code != 0 {
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
}
if !strings.Contains(stderr.String(), "cross-server ref index: 1 live keys") {
t.Fatalf("stderr = %q, want cross-server index log", stderr.String())
}
data, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
var snapshot struct {
Tools map[string]map[string]any `json:"tools"`
}
if err := json.Unmarshal(data, &snapshot); err != nil {
t.Fatal(err)
}
for _, canonical := range []string{"aitable.advperm_enable", "aitable.advperm_disable"} {
entry := snapshot.Tools[canonical]
if entry["title"] != "live title" || entry["description"] != "live desc" {
t.Fatalf("%s = %#v, want live refresh", canonical, entry)
}
ref := entry["interface_ref"].(map[string]any)
if ref["product_id"] != "aitable-helper" || ref["rpc_name"] != "set_advanced_permission" {
t.Fatalf("%s reviewed ref lost: %#v", canonical, ref)
}
}
}
// TestRunCrossOwnedCanonicalIgnoresNameCoincidence:canonical 拥有评审过的
// 跨 server 身份时,另一 server 上恰好同名的工具不得直连覆盖其元数据——
// 数据源只能是评审身份指向的 live 工具。
func TestRunCrossOwnedCanonicalIgnoresNameCoincidence(t *testing.T) {
registry := func() (map[string]map[string]string, error) {
return map[string]map[string]string{
"aitable.advperm_enable": {"product_id": "aitable", "rpc_name": "advperm_enable"},
}, nil
}
servers := []syncdata.ServerInfo{
{ID: "aitable", Endpoint: "https://aitable.example"},
{ID: "aitable-helper", Endpoint: "https://helper.example"},
}
lister := &fakeLister{
results: map[string]transport.ToolsListResult{
// 同名巧合:aitable server 上恰好也有 advperm_enable。
"https://aitable.example": {Tools: []transport.ToolDescriptor{
{Name: "advperm_enable", Title: "coincidence title", Description: "coincidence desc"},
}},
"https://helper.example": {Tools: []transport.ToolDescriptor{
{Name: "set_advanced_permission", Title: "owner title", Description: "owner desc"},
}},
},
}
stubDeps(t, "env-token", nil, servers, lister, registry)
output := filepath.Join(t.TempDir(), "snapshot.json")
prev := `{"tools":{"aitable.advperm_enable":{"title":"stale","interface_ref":{"product_id":"aitable-helper","rpc_name":"set_advanced_permission"}}}}`
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
t.Fatal(err)
}
var stderr bytes.Buffer
if code := run([]string{"--output", output}, &stderr); code != 0 {
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
}
data, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
var snapshot struct {
Tools map[string]map[string]any `json:"tools"`
}
if err := json.Unmarshal(data, &snapshot); err != nil {
t.Fatal(err)
}
entry := snapshot.Tools["aitable.advperm_enable"]
if entry["title"] != "owner title" || entry["description"] != "owner desc" {
t.Fatalf("entry = %#v, want reviewed-identity source to win over name coincidence", entry)
}
}
func TestMergeLiveMCPToolRefreshesExistingMetadata(t *testing.T) {
const canonical = "calendar.list_calendars"
reviewedRef := map[string]any{
"product_id": "calendar-helper",
"rpc_name": "list_user_calendars",
}
allTools := map[string]map[string]any{
canonical: {
"title": "old title",
"description": "old description",
"interface_ref": reviewedRef,
"parameters": map[string]any{
"stale": map[string]any{"type": "string"},
},
},
}
live := transport.ToolDescriptor{
Name: "list_calendars",
Title: "new title",
Description: "new description",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"cursor": map[string]any{
"type": "string",
"description": "next page cursor",
},
},
"required": []any{"cursor"},
},
}
fallbackRef := map[string]string{
"product_id": "calendar",
"rpc_name": "list_calendars",
}
mergeLiveMCPTool(allTools, canonical, live, fallbackRef)
got := allTools[canonical]
if got["title"] != "new title" || got["description"] != "new description" {
t.Fatalf("live metadata was not refreshed: %#v", got)
}
if !reflect.DeepEqual(got["interface_ref"], reviewedRef) {
t.Fatalf("interface_ref = %#v, want reviewed mapping %#v", got["interface_ref"], reviewedRef)
}
params, ok := got["parameters"].(map[string]map[string]any)
if !ok {
t.Fatalf("parameters type = %T, want refreshed parameter map", got["parameters"])
}
if _, stale := params["stale"]; stale {
t.Fatalf("stale parameter survived refresh: %#v", params)
}
if cursor := params["cursor"]; cursor["type"] != "string" || cursor["description"] != "next page cursor" || cursor["required"] != true {
t.Fatalf("cursor parameter = %#v", cursor)
}
}
func TestBuildCoverageReportsFailedServices(t *testing.T) {
got := buildCoverage(26, []string{"doc", "sheet"}, 800, 813, 40)
if got["source_services"] != 26 {
t.Fatalf("source_services = %v, want 26", got["source_services"])
}
if got["snapshot_services"] != 24 {
t.Fatalf("snapshot_services = %v, want 24 (26 sources - 2 failures)", got["snapshot_services"])
}
if !reflect.DeepEqual(got["missing_services"], []string{"doc", "sheet"}) {
t.Fatalf("missing_services = %#v, want failed service IDs", got["missing_services"])
}
// matched 必须剔除 stub 占位,unmatched 据实等于 stub 数。
if got["matched_tools"] != 773 || got["unmatched_tools"] != 40 {
t.Fatalf("matched/unmatched = %v/%v, want 773/40 (813 surface - 40 stubs)", got["matched_tools"], got["unmatched_tools"])
}
if got["source_tools"] != 800 || got["surface_tools"] != 813 {
t.Fatalf("tool counts = %#v", got)
}
}
func TestBuildCoverageFullSnapshotHasNoMissingServices(t *testing.T) {
got := buildCoverage(26, nil, 813, 813, 0)
if got["snapshot_services"] != 26 {
t.Fatalf("snapshot_services = %v, want 26", got["snapshot_services"])
}
if !reflect.DeepEqual(got["missing_services"], []string{}) {
t.Fatalf("missing_services = %#v, want empty non-nil slice", got["missing_services"])
}
if got["matched_tools"] != 813 || got["unmatched_tools"] != 0 {
t.Fatalf("matched/unmatched = %v/%v, want 813/0 for stub-free snapshot", got["matched_tools"], got["unmatched_tools"])
}
}
// fakeLister returns canned tools/list results per endpoint.
type fakeLister struct {
results map[string]transport.ToolsListResult
errs map[string]error
}
func (f *fakeLister) ListTools(_ context.Context, endpoint string) (transport.ToolsListResult, error) {
if err := f.errs[endpoint]; err != nil {
return transport.ToolsListResult{}, err
}
return f.results[endpoint], nil
}
// stubDeps swaps every injection point for the duration of one test.
func stubDeps(t *testing.T, token string, keychain func() (*auth.TokenData, error), servers []syncdata.ServerInfo, lister toolLister, registry func() (map[string]map[string]string, error)) {
t.Helper()
origGetenv, origLoad, origServers, origNew, origRegistry := getenv, loadTokenData, staticServers, newToolLister, registrySource
t.Cleanup(func() {
getenv, loadTokenData, staticServers, newToolLister, registrySource = origGetenv, origLoad, origServers, origNew, origRegistry
})
getenv = func(key string) string {
if key == "DWS_ACCESS_TOKEN" {
return token
}
return ""
}
loadTokenData = keychain
staticServers = func() []syncdata.ServerInfo { return servers }
newToolLister = func(string) toolLister { return lister }
registrySource = registry
}
func testRegistryRefs() (map[string]map[string]string, error) {
return map[string]map[string]string{
"doc.copy_document": {"product_id": "doc", "rpc_name": "copy_document"},
"doc.get_document": {"product_id": "doc", "rpc_name": "get_document"},
}, nil
}
func TestCrossPlatformCoverageRunRefusesRetiredPinnedMCPMetadataPath(t *testing.T) {
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
var stderr bytes.Buffer
code := run([]string{"--output", "internal/cli/schema_mcp_metadata.json"}, &stderr)
if code != 2 {
t.Fatalf("run(retired pin) = %d, want 2", code)
}
if !strings.Contains(stderr.String(), "refusing to write retired Schema pin") {
t.Fatalf("stderr = %q, want retired-pin refusal", stderr.String())
}
if !retiredPinnedMCPMetadataPath("internal/cli/schema_mcp_metadata.json") ||
!retiredPinnedMCPMetadataPath("/tmp/repo/internal/cli/schema_mcp_metadata.json") ||
retiredPinnedMCPMetadataPath("artifacts/mcp_metadata_diagnostic.json") {
t.Fatal("retiredPinnedMCPMetadataPath classification is incorrect")
}
}
func TestRunNoTokenFails(t *testing.T) {
stubDeps(t, "", func() (*auth.TokenData, error) { return nil, errors.New("no keychain") }, nil, &fakeLister{}, testRegistryRefs)
var stderr bytes.Buffer
if code := run(nil, &stderr); code != 1 {
t.Fatalf("run() = %d, want 1", code)
}
if !strings.Contains(stderr.String(), "no auth token") {
t.Fatalf("stderr = %q, want no-auth-token hint", stderr.String())
}
}
func TestRunInvalidFlagFails(t *testing.T) {
stubDeps(t, "tok", nil, nil, &fakeLister{}, testRegistryRefs)
var stderr bytes.Buffer
if code := run([]string{"--nonexistent"}, &stderr); code != 2 {
t.Fatalf("run() = %d, want 2", code)
}
}
func TestResolveTokenKeychainFallback(t *testing.T) {
stubDeps(t, "", func() (*auth.TokenData, error) {
return &auth.TokenData{AccessToken: "kc-token"}, nil
}, nil, &fakeLister{}, testRegistryRefs)
var stderr bytes.Buffer
if got := resolveToken(&stderr); got != "kc-token" {
t.Fatalf("resolveToken() = %q, want kc-token", got)
}
if !strings.Contains(stderr.String(), "loaded token from keychain") {
t.Fatalf("stderr = %q, want keychain log", stderr.String())
}
}
func TestResolveTokenEmptyKeychainToken(t *testing.T) {
stubDeps(t, "", func() (*auth.TokenData, error) { return &auth.TokenData{}, nil }, nil, &fakeLister{}, testRegistryRefs)
var stderr bytes.Buffer
if got := resolveToken(&stderr); got != "" {
t.Fatalf("resolveToken() = %q, want empty", got)
}
}
func TestCrossPlatformCoverageRunWritesSnapshotWithHonestCoverage(t *testing.T) {
servers := []syncdata.ServerInfo{
{ID: "doc", Endpoint: "https://doc.example"},
{ID: "sheet", Endpoint: "https://sheet.example"},
{ID: "blank", Endpoint: " "},
}
lister := &fakeLister{
results: map[string]transport.ToolsListResult{
"https://doc.example": {Tools: []transport.ToolDescriptor{
{Name: "copy_document", Title: "复制文档", Description: "copy", InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"doc_id": map[string]any{"type": "string", "description": "文档 ID", "default": "d", "enum": []any{"a", "b", 3}},
"bogus": "not-a-map",
},
"required": []any{"doc_id", 42},
}},
{Name: " "},
{Name: "not_in_registry"},
}},
},
errs: map[string]error{"https://sheet.example": errors.New("boom")},
}
stubDeps(t, "env-token", nil, servers, lister, testRegistryRefs)
dir := t.TempDir()
output := filepath.Join(dir, "snapshot.json")
prev := `{"tools":{"doc.get_document":{"interface_ref":{"product_id":"doc-helper","rpc_name":"fetch_document"}}}}`
if err := os.WriteFile(output, []byte(prev), 0o600); err != nil {
t.Fatal(err)
}
var stderr bytes.Buffer
if code := run([]string{"--output", output}, &stderr); code != 0 {
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
}
data, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
var snapshot struct {
Version int `json:"version"`
Coverage map[string]any `json:"coverage"`
Tools map[string]map[string]any
}
if err := json.Unmarshal(data, &snapshot); err != nil {
t.Fatal(err)
}
if snapshot.Version != 1 {
t.Fatalf("version = %d", snapshot.Version)
}
if got := snapshot.Coverage["snapshot_services"].(float64); got != 2 {
t.Fatalf("snapshot_services = %v, want 2 (3 servers - 1 failed; blank endpoint not counted as failed)", got)
}
if got := snapshot.Coverage["missing_services"].([]any); len(got) != 1 || got[0] != "sheet" {
t.Fatalf("missing_services = %v, want [sheet]", got)
}
live := snapshot.Tools["doc.copy_document"]
if live == nil || live["title"] != "复制文档" {
t.Fatalf("doc.copy_document = %#v, want live metadata", live)
}
params := live["parameters"].(map[string]any)
docID := params["doc_id"].(map[string]any)
if docID["type"] != "string" || docID["required"] != true || docID["default"] != "d" {
t.Fatalf("doc_id = %#v", docID)
}
if enum := docID["enum"].([]any); len(enum) != 2 {
t.Fatalf("enum = %v, want the 2 string members only", enum)
}
if _, ok := params["bogus"]; ok {
t.Fatal("non-map property should be skipped")
}
prevRef := snapshot.Tools["doc.get_document"]["interface_ref"].(map[string]any)
if prevRef["product_id"] != "doc-helper" {
t.Fatalf("previous reviewed ref lost: %#v", prevRef)
}
if _, ok := snapshot.Tools["not_in_registry"]; ok {
t.Fatal("tools outside the registry must be dropped")
}
if !strings.Contains(stderr.String(), "services unreachable: sheet") {
t.Fatalf("stderr = %q, want unreachable log", stderr.String())
}
}
func TestRunIgnoresCorruptPreviousSnapshot(t *testing.T) {
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
output := filepath.Join(t.TempDir(), "snapshot.json")
if err := os.WriteFile(output, []byte("{corrupt"), 0o600); err != nil {
t.Fatal(err)
}
var stderr bytes.Buffer
if code := run([]string{"--output", output}, &stderr); code != 0 {
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
}
}
func TestCrossPlatformCoverageRunRegistryLoadFailureStillWritesStublessSnapshot(t *testing.T) {
stubDeps(t, "env-token", nil, nil, &fakeLister{}, func() (map[string]map[string]string, error) { return nil, errors.New("no identity") })
output := filepath.Join(t.TempDir(), "snapshot.json")
var stderr bytes.Buffer
if code := run([]string{"--output", output}, &stderr); code != 0 {
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
}
if !strings.Contains(stderr.String(), "cannot collect command identity") {
t.Fatalf("stderr = %q, want identity collection warning", stderr.String())
}
}
func TestRunWriteFailure(t *testing.T) {
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
var stderr bytes.Buffer
badPath := filepath.Join(t.TempDir(), "missing-dir", "snapshot.json")
if code := run([]string{"--output", badPath}, &stderr); code != 1 {
t.Fatalf("run() = %d, want 1 on write failure", code)
}
}
func TestWriteMetadataMarshalFailure(t *testing.T) {
err := writeMetadata(filepath.Join(t.TempDir(), "out.json"), map[string]any{"bad": math.NaN()})
if err == nil || !strings.Contains(err.Error(), "marshal failed") {
t.Fatalf("err = %v, want marshal failure", err)
}
}
func TestMainDelegatesToRun(t *testing.T) {
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
origExit, origArgs := osExit, os.Args
t.Cleanup(func() { osExit, os.Args = origExit, origArgs })
exitCode := -1
osExit = func(code int) { exitCode = code }
os.Args = []string{"fetch_mcp_metadata", "--output", filepath.Join(t.TempDir(), "snapshot.json")}
main()
if exitCode != 0 {
t.Fatalf("main() exited with %d, want 0", exitCode)
}
}
func TestExtractParamsNilAndNonObjectSchemas(t *testing.T) {
if got := extractParams(nil); got != nil {
t.Fatalf("extractParams(nil) = %v, want nil", got)
}
if got := extractParams(map[string]any{"type": "object"}); got != nil {
t.Fatalf("extractParams(no properties) = %v, want nil", got)
}
}
func TestCrossPlatformCoverageNewToolListerBuildsAuthedClient(t *testing.T) {
if lister := newToolLister("tok"); lister == nil {
t.Fatal("newToolLister returned nil")
}
}
func TestRunRecordsSourceRevision(t *testing.T) {
stubDeps(t, "env-token", nil, nil, &fakeLister{}, testRegistryRefs)
dir := t.TempDir()
head := filepath.Join(dir, "HEAD")
if err := os.WriteFile(head, []byte("ref: refs/heads/feature\n"), 0o600); err != nil {
t.Fatal(err)
}
origHead := gitHeadPath
t.Cleanup(func() { gitHeadPath = origHead })
gitHeadPath = head
output := filepath.Join(dir, "snapshot.json")
var stderr bytes.Buffer
if code := run([]string{"--output", output}, &stderr); code != 0 {
t.Fatalf("run() = %d, stderr=%s", code, stderr.String())
}
data, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
var snapshot struct {
SourceRevision string `json:"source_revision"`
}
if err := json.Unmarshal(data, &snapshot); err != nil {
t.Fatal(err)
}
if snapshot.SourceRevision != "ref: refs/heads/feature" {
t.Fatalf("source_revision = %q", snapshot.SourceRevision)
}
}