feat(manifest): add embedded runtime fallback for scaffolded apps
This commit is contained in:
parent
01c0c7e1bc
commit
a9378885f2
10 changed files with 217 additions and 17 deletions
|
|
@ -55,6 +55,7 @@ type DoctorOptions struct {
|
||||||
SecretStoreFactory func() (secretstore.Store, error)
|
SecretStoreFactory func() (secretstore.Store, error)
|
||||||
ManifestDir string
|
ManifestDir string
|
||||||
ManifestValidator DoctorManifestValidator
|
ManifestValidator DoctorManifestValidator
|
||||||
|
ManifestCheck DoctorCheck
|
||||||
ConnectivityCheck DoctorCheck
|
ConnectivityCheck DoctorCheck
|
||||||
ExtraChecks []DoctorCheck
|
ExtraChecks []DoctorCheck
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +72,11 @@ func RunDoctor(ctx context.Context, options DoctorOptions) DoctorReport {
|
||||||
if len(options.RequiredSecrets) > 0 && options.SecretStoreFactory != nil {
|
if len(options.RequiredSecrets) > 0 && options.SecretStoreFactory != nil {
|
||||||
checks = append(checks, RequiredSecretsCheck(options.SecretStoreFactory, options.RequiredSecrets))
|
checks = append(checks, RequiredSecretsCheck(options.SecretStoreFactory, options.RequiredSecrets))
|
||||||
}
|
}
|
||||||
checks = append(checks, ManifestCheck(options.ManifestDir, options.ManifestValidator))
|
if options.ManifestCheck != nil {
|
||||||
|
checks = append(checks, options.ManifestCheck)
|
||||||
|
} else {
|
||||||
|
checks = append(checks, ManifestCheck(options.ManifestDir, options.ManifestValidator))
|
||||||
|
}
|
||||||
if options.ConnectivityCheck != nil {
|
if options.ConnectivityCheck != nil {
|
||||||
checks = append(checks, options.ConnectivityCheck)
|
checks = append(checks, options.ConnectivityCheck)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,31 @@ func TestManifestCheckUsesValidator(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunDoctorUsesCustomManifestCheckWhenProvided(t *testing.T) {
|
||||||
|
report := RunDoctor(context.Background(), DoctorOptions{
|
||||||
|
ManifestDir: t.TempDir(),
|
||||||
|
ManifestCheck: func(context.Context) DoctorResult {
|
||||||
|
return DoctorResult{
|
||||||
|
Name: "manifest",
|
||||||
|
Status: DoctorStatusOK,
|
||||||
|
Summary: "manifest is embedded",
|
||||||
|
Detail: "embedded:mcp.toml",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(report.Results) != 1 {
|
||||||
|
t.Fatalf("result count = %d, want 1", len(report.Results))
|
||||||
|
}
|
||||||
|
result := report.Results[0]
|
||||||
|
if result.Summary != "manifest is embedded" {
|
||||||
|
t.Fatalf("summary = %q", result.Summary)
|
||||||
|
}
|
||||||
|
if result.Detail != "embedded:mcp.toml" {
|
||||||
|
t.Fatalf("detail = %q", result.Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderDoctorReportFormatsStatusesAndSummary(t *testing.T) {
|
func TestRenderDoctorReportFormatsStatusesAndSummary(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
report := DoctorReport{
|
report := DoctorReport{
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,6 @@ Le flux typique côté application est :
|
||||||
2. Résoudre le profil actif avec `cli`.
|
2. Résoudre le profil actif avec `cli`.
|
||||||
3. Charger la config versionnée avec `config`.
|
3. Charger la config versionnée avec `config`.
|
||||||
4. Lire les secrets avec `secretstore`.
|
4. Lire les secrets avec `secretstore`.
|
||||||
5. Charger `mcp.toml` avec `manifest`.
|
5. Charger le manifest runtime avec `manifest` (`mcp.toml` local, ou fallback embarqué).
|
||||||
6. Exécuter l'auto-update avec `update` si nécessaire.
|
6. Exécuter l'auto-update avec `update` si nécessaire.
|
||||||
7. Exécuter `doctor` pour diagnostiquer la configuration locale et brancher des checks métier.
|
7. Exécuter `doctor` pour diagnostiquer la configuration locale et brancher des checks métier.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Manifeste `mcp.toml`
|
# Manifeste `mcp.toml`
|
||||||
|
|
||||||
Le package `manifest` cherche automatiquement `mcp.toml` dans le répertoire courant puis remonte les répertoires parents jusqu'à trouver le fichier.
|
Le package `manifest` cherche automatiquement `mcp.toml` dans le répertoire courant puis remonte les répertoires parents jusqu'à trouver le fichier.
|
||||||
|
Pour un binaire installé (par exemple dans `~/.local/bin`), il peut aussi charger un fallback embarqué via `LoadDefaultOrEmbedded`.
|
||||||
|
|
||||||
Exemple minimal :
|
Exemple minimal :
|
||||||
|
|
||||||
|
|
@ -81,3 +82,14 @@ _ = bootstrapInfo
|
||||||
_ = scaffoldInfo
|
_ = scaffoldInfo
|
||||||
_ = source
|
_ = source
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Fallback fichier puis embarqué :
|
||||||
|
|
||||||
|
```go
|
||||||
|
file, source, err := manifest.LoadDefaultOrEmbedded(".", embeddedManifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("manifest source: %s\n", source) // chemin du fichier ou "embedded:mcp.toml"
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ type Profile struct {
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var embeddedManifest = `...` // fallback utilisé si aucun mcp.toml runtime n'est trouvé
|
||||||
|
|
||||||
func run(ctx context.Context, flagProfile string) error {
|
func run(ctx context.Context, flagProfile string) error {
|
||||||
cfgStore := config.NewStore[Profile]("my-mcp")
|
cfgStore := config.NewStore[Profile]("my-mcp")
|
||||||
|
|
||||||
|
|
@ -16,7 +18,7 @@ func run(ctx context.Context, flagProfile string) error {
|
||||||
profileName := cli.ResolveProfileName(flagProfile, os.Getenv("MY_MCP_PROFILE"), cfg.CurrentProfile)
|
profileName := cli.ResolveProfileName(flagProfile, os.Getenv("MY_MCP_PROFILE"), cfg.CurrentProfile)
|
||||||
profile := cfg.Profiles[profileName]
|
profile := cfg.Profiles[profileName]
|
||||||
|
|
||||||
manifestFile, _, err := manifest.LoadDefault(".")
|
manifestFile, _, err := manifest.LoadDefaultOrEmbedded(".", embeddedManifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
- `bootstrap` : couche CLI optionnelle avec sous-commandes communes (`setup`, `mcp`, `config show|test`, `update`, `version`) et hooks métier explicites.
|
- `bootstrap` : couche CLI optionnelle avec sous-commandes communes (`setup`, `mcp`, `config show|test`, `update`, `version`) et hooks métier explicites.
|
||||||
- `cli` : helpers pour résoudre un profil, valider une URL, demander des valeurs à l'utilisateur et exécuter un `doctor`.
|
- `cli` : helpers pour résoudre un profil, valider une URL, demander des valeurs à l'utilisateur et exécuter un `doctor`.
|
||||||
- `config` : lecture/écriture atomique d'une config JSON versionnée dans `os.UserConfigDir()`.
|
- `config` : lecture/écriture atomique d'une config JSON versionnée dans `os.UserConfigDir()`.
|
||||||
- `manifest` : lecture de `mcp.toml` à la racine du projet, conversion vers `update.ReleaseSource` et exposition de métadonnées pour `bootstrap`/scaffolding.
|
- `manifest` : lecture de `mcp.toml` à la racine du projet, fallback embarqué pour le runtime, conversion vers `update.ReleaseSource` et exposition de métadonnées pour `bootstrap`/scaffolding.
|
||||||
- `scaffold` : génération d'un squelette de projet MCP (arborescence, `main.go`, `mcp.toml`, `install.sh` wizard, wiring de base et README de démarrage).
|
- `scaffold` : génération d'un squelette de projet MCP (arborescence, `main.go`, `mcp.toml`, `install.sh` wizard, wiring de base et README de démarrage).
|
||||||
- `secretstore` : lecture/écriture de secrets dans le wallet natif, avec helper runtime `OpenFromManifest`.
|
- `secretstore` : lecture/écriture de secrets dans le wallet natif, avec helper runtime `OpenFromManifest`.
|
||||||
- `update` : téléchargement et remplacement du binaire courant depuis un endpoint de release.
|
- `update` : téléchargement et remplacement du binaire courant depuis un endpoint de release.
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const DefaultFile = "mcp.toml"
|
const DefaultFile = "mcp.toml"
|
||||||
|
const EmbeddedSource = "embedded:mcp.toml"
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
BinaryName string `toml:"binary_name"`
|
BinaryName string `toml:"binary_name"`
|
||||||
|
|
@ -118,9 +119,39 @@ func Load(path string) (File, error) {
|
||||||
return File{}, fmt.Errorf("read manifest %s: %w", path, err)
|
return File{}, fmt.Errorf("read manifest %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return parse(data, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadEmbedded(content string) (File, string, error) {
|
||||||
|
trimmed := strings.TrimSpace(content)
|
||||||
|
if trimmed == "" {
|
||||||
|
return File{}, "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := parse([]byte(trimmed), EmbeddedSource)
|
||||||
|
if err != nil {
|
||||||
|
return File{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, EmbeddedSource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadDefaultOrEmbedded(startDir, embeddedContent string) (File, string, error) {
|
||||||
|
file, path, err := LoadDefault(startDir)
|
||||||
|
if err == nil {
|
||||||
|
return file, path, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return File{}, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadEmbedded(embeddedContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(data []byte, source string) (File, error) {
|
||||||
var file File
|
var file File
|
||||||
if err := toml.Unmarshal(data, &file); err != nil {
|
if err := toml.Unmarshal(data, &file); err != nil {
|
||||||
return File{}, fmt.Errorf("parse manifest %s: %w", path, err)
|
return File{}, fmt.Errorf("parse manifest %s: %w", source, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
file.normalize()
|
file.normalize()
|
||||||
|
|
|
||||||
|
|
@ -234,3 +234,64 @@ description = " Client MCP interne "
|
||||||
t.Fatalf("scaffold known environment variables = %v", scaffold.KnownEnvironmentVariables)
|
t.Fatalf("scaffold known environment variables = %v", scaffold.KnownEnvironmentVariables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadEmbeddedParsesContent(t *testing.T) {
|
||||||
|
file, source, err := LoadEmbedded(`
|
||||||
|
[update]
|
||||||
|
latest_release_url = "https://example.com/latest"
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadEmbedded returned error: %v", err)
|
||||||
|
}
|
||||||
|
if source != EmbeddedSource {
|
||||||
|
t.Fatalf("source = %q, want %q", source, EmbeddedSource)
|
||||||
|
}
|
||||||
|
if file.Update.LatestReleaseURL != "https://example.com/latest" {
|
||||||
|
t.Fatalf("latest release URL = %q", file.Update.LatestReleaseURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadEmbeddedReturnsNotExistWhenEmpty(t *testing.T) {
|
||||||
|
_, _, err := LoadEmbedded(" ")
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("error = %v, want os.ErrNotExist", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultOrEmbeddedPrefersManifestFile(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
path := filepath.Join(root, DefaultFile)
|
||||||
|
if err := os.WriteFile(path, []byte("[update]\nlatest_release_url = \"https://example.com/from-file\"\n"), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, source, err := LoadDefaultOrEmbedded(root, `
|
||||||
|
[update]
|
||||||
|
latest_release_url = "https://example.com/from-embedded"
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadDefaultOrEmbedded returned error: %v", err)
|
||||||
|
}
|
||||||
|
if source != path {
|
||||||
|
t.Fatalf("source = %q, want %q", source, path)
|
||||||
|
}
|
||||||
|
if file.Update.LatestReleaseURL != "https://example.com/from-file" {
|
||||||
|
t.Fatalf("latest release URL = %q", file.Update.LatestReleaseURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultOrEmbeddedUsesEmbeddedWhenFileMissing(t *testing.T) {
|
||||||
|
file, source, err := LoadDefaultOrEmbedded(t.TempDir(), `
|
||||||
|
[update]
|
||||||
|
latest_release_url = "https://example.com/from-embedded"
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadDefaultOrEmbedded returned error: %v", err)
|
||||||
|
}
|
||||||
|
if source != EmbeddedSource {
|
||||||
|
t.Fatalf("source = %q, want %q", source, EmbeddedSource)
|
||||||
|
}
|
||||||
|
if file.Update.LatestReleaseURL != "https://example.com/from-embedded" {
|
||||||
|
t.Fatalf("latest release URL = %q", file.Update.LatestReleaseURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -888,6 +888,38 @@ import (
|
||||||
"gitea.lclr.dev/AI/mcp-framework/update"
|
"gitea.lclr.dev/AI/mcp-framework/update"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var embeddedManifest = ` + "`" + `binary_name = "{{.BinaryName}}"
|
||||||
|
docs_url = "{{.DocsURL}}"
|
||||||
|
|
||||||
|
[update]
|
||||||
|
source_name = "Release endpoint"
|
||||||
|
driver = "{{.ReleaseDriver}}"
|
||||||
|
repository = "{{.ReleaseRepository}}"
|
||||||
|
base_url = "{{.ReleaseBaseURL}}"
|
||||||
|
asset_name_template = "{binary}-{os}-{arch}{ext}"
|
||||||
|
checksum_asset_name = "{asset}.sha256"
|
||||||
|
checksum_required = true
|
||||||
|
signature_asset_name = "{asset}.sig"
|
||||||
|
signature_required = false
|
||||||
|
signature_public_key_env_names = ["{{.ReleasePublicKeyEnv}}"]
|
||||||
|
token_header = "Authorization"
|
||||||
|
token_prefix = "token"
|
||||||
|
token_env_names = ["{{.ReleaseTokenEnv}}"]
|
||||||
|
|
||||||
|
[environment]
|
||||||
|
known = [{{- range $index, $value := .KnownEnvironmentVariables}}{{if $index}}, {{end}}"{{$value}}"{{- end}}]
|
||||||
|
|
||||||
|
[secret_store]
|
||||||
|
backend_policy = "{{.SecretStorePolicy}}"
|
||||||
|
|
||||||
|
[profiles]
|
||||||
|
default = "{{.DefaultProfile}}"
|
||||||
|
known = [{{- range $index, $value := .Profiles}}{{if $index}}, {{end}}"{{$value}}"{{- end}}]
|
||||||
|
|
||||||
|
[bootstrap]
|
||||||
|
description = "{{.Description}}"
|
||||||
|
` + "`" + `
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
BaseURL string
|
BaseURL string
|
||||||
}
|
}
|
||||||
|
|
@ -895,6 +927,7 @@ type Profile struct {
|
||||||
type Runtime struct {
|
type Runtime struct {
|
||||||
ConfigStore config.Store[Profile]
|
ConfigStore config.Store[Profile]
|
||||||
Manifest manifest.File
|
Manifest manifest.File
|
||||||
|
ManifestSource string
|
||||||
BinaryName string
|
BinaryName string
|
||||||
Description string
|
Description string
|
||||||
Version string
|
Version string
|
||||||
|
|
@ -921,12 +954,13 @@ func NewRuntime(version string) (Runtime, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manifestFile, _, err := manifest.LoadDefault(manifestStartDir)
|
manifestFile, manifestSource, err := manifest.LoadDefaultOrEmbedded(manifestStartDir, embeddedManifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return Runtime{}, err
|
return Runtime{}, err
|
||||||
}
|
}
|
||||||
manifestFile = manifest.File{}
|
manifestFile = manifest.File{}
|
||||||
|
manifestSource = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapInfo := manifestFile.BootstrapInfo()
|
bootstrapInfo := manifestFile.BootstrapInfo()
|
||||||
|
|
@ -944,13 +978,14 @@ func NewRuntime(version string) (Runtime, error) {
|
||||||
tokenEnv = firstNonEmpty(scaffoldInfo.KnownEnvironmentVariables[2], tokenEnv)
|
tokenEnv = firstNonEmpty(scaffoldInfo.KnownEnvironmentVariables[2], tokenEnv)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Runtime{
|
return Runtime{
|
||||||
ConfigStore: config.NewStore[Profile](binaryName),
|
ConfigStore: config.NewStore[Profile](binaryName),
|
||||||
Manifest: manifestFile,
|
Manifest: manifestFile,
|
||||||
BinaryName: binaryName,
|
ManifestSource: manifestSource,
|
||||||
Description: description,
|
BinaryName: binaryName,
|
||||||
Version: firstNonEmpty(strings.TrimSpace(version), "dev"),
|
Description: description,
|
||||||
DefaultProfile: defaultProfile,
|
Version: firstNonEmpty(strings.TrimSpace(version), "dev"),
|
||||||
|
DefaultProfile: defaultProfile,
|
||||||
ProfileEnv: profileEnv,
|
ProfileEnv: profileEnv,
|
||||||
TokenEnv: tokenEnv,
|
TokenEnv: tokenEnv,
|
||||||
SecretName: binaryName + "-api-token",
|
SecretName: binaryName + "-api-token",
|
||||||
|
|
@ -1113,7 +1148,7 @@ func (r Runtime) runConfigTest(ctx context.Context, inv bootstrap.Invocation) er
|
||||||
{Name: r.SecretName, Label: "API token"},
|
{Name: r.SecretName, Label: "API token"},
|
||||||
},
|
},
|
||||||
SecretStoreFactory: r.openSecretStore,
|
SecretStoreFactory: r.openSecretStore,
|
||||||
ManifestDir: ".",
|
ManifestCheck: r.manifestDoctorCheck(),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := cli.RenderDoctorReport(stdout, report); err != nil {
|
if err := cli.RenderDoctorReport(stdout, report); err != nil {
|
||||||
|
|
@ -1142,8 +1177,14 @@ func (r Runtime) runUpdate(ctx context.Context, inv bootstrap.Invocation) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Runtime) openSecretStore() (secretstore.Store, error) {
|
func (r Runtime) openSecretStore() (secretstore.Store, error) {
|
||||||
return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
|
backendPolicy := secretstore.BackendPolicy(strings.TrimSpace(r.Manifest.SecretStore.BackendPolicy))
|
||||||
|
if backendPolicy == "" {
|
||||||
|
backendPolicy = secretstore.BackendAuto
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretstore.Open(secretstore.Options{
|
||||||
ServiceName: r.BinaryName,
|
ServiceName: r.BinaryName,
|
||||||
|
BackendPolicy: backendPolicy,
|
||||||
LookupEnv: func(name string) (string, bool) {
|
LookupEnv: func(name string) (string, bool) {
|
||||||
if name == r.SecretName {
|
if name == r.SecretName {
|
||||||
return os.LookupEnv(r.TokenEnv)
|
return os.LookupEnv(r.TokenEnv)
|
||||||
|
|
@ -1179,6 +1220,26 @@ func firstNonEmpty(values ...string) string {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r Runtime) manifestDoctorCheck() cli.DoctorCheck {
|
||||||
|
return func(context.Context) cli.DoctorResult {
|
||||||
|
source := strings.TrimSpace(r.ManifestSource)
|
||||||
|
if source == "" {
|
||||||
|
return cli.DoctorResult{
|
||||||
|
Name: "manifest",
|
||||||
|
Status: cli.DoctorStatusWarn,
|
||||||
|
Summary: "manifest is missing, using built-in defaults",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.DoctorResult{
|
||||||
|
Name: "manifest",
|
||||||
|
Status: cli.DoctorStatusOK,
|
||||||
|
Summary: "manifest is valid",
|
||||||
|
Detail: source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const manifestTemplate = `binary_name = "{{.BinaryName}}"
|
const manifestTemplate = `binary_name = "{{.BinaryName}}"
|
||||||
|
|
|
||||||
|
|
@ -66,12 +66,15 @@ func TestGenerateCreatesRecommendedSkeleton(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, snippet := range []string{
|
for _, snippet := range []string{
|
||||||
"config.NewStore[Profile]",
|
"config.NewStore[Profile]",
|
||||||
"secretstore.OpenFromManifest",
|
"secretstore.Open(secretstore.Options",
|
||||||
"update.Run",
|
"update.Run",
|
||||||
"manifest.LoadDefault",
|
"manifest.LoadDefaultOrEmbedded",
|
||||||
"bootstrap.Run",
|
"bootstrap.Run",
|
||||||
"os.Executable()",
|
"os.Executable()",
|
||||||
"errors.Is(err, os.ErrNotExist)",
|
"errors.Is(err, os.ErrNotExist)",
|
||||||
|
`var embeddedManifest = `,
|
||||||
|
"ManifestSource",
|
||||||
|
"ManifestCheck: r.manifestDoctorCheck()",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(string(appGo), snippet) {
|
if !strings.Contains(string(appGo), snippet) {
|
||||||
t.Fatalf("app.go missing snippet %q", snippet)
|
t.Fatalf("app.go missing snippet %q", snippet)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue