mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
79eb16c50b
Co-authored-by: liangshuo-1 <266696938+liangshuo-1@users.noreply.github.com>
534 lines
19 KiB
Go
534 lines
19 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package plugin_e2e
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// skillSelectionPlugin proves that a plugin's Allow/Remove selection composes
|
|
// against the base embedded by the external wrapper main. Before the wrapper
|
|
// wired cmd.SetEmbeddedSkillContent, this configuration installed a fatal guard
|
|
// that made every command exit 2.
|
|
const skillSelectionPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("skill-selection", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{
|
|
Allow: []string{"lark-a", "lark-b"},
|
|
Remove: []string{"lark-b"},
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
const incompleteSkillDependencyPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("incomplete-skill-dependency", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{
|
|
Allow: []string{"lark-doc"},
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
const completeSkillDependencyPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("complete-skill-dependency", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{
|
|
Allow: []string{"lark-doc", "lark-shared"},
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
const replacementSkillTreePlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import (
|
|
"testing/fstest"
|
|
|
|
"github.com/larksuite/cli/extension/platform"
|
|
)
|
|
|
|
func init() {
|
|
base := fstest.MapFS{
|
|
"acme-docx/SKILL.md": &fstest.MapFile{Data: []byte("---\nname: acme-docx\ndescription: custom docs\n---\n")},
|
|
"acme-docx/guides/create.md": &fstest.MapFile{Data: []byte("# acme create\n")},
|
|
"acme-docx/references/lark-doc-fetch.md": &fstest.MapFile{Data: []byte("# acme fetch\n")},
|
|
"acme-docx/references/lark-doc-history.md": &fstest.MapFile{Data: []byte("# acme history\n")},
|
|
"acme-docx/references/lark-doc-md.md": &fstest.MapFile{Data: []byte("# acme markdown\n")},
|
|
"acme-docx/references/lark-doc-update.md": &fstest.MapFile{Data: []byte("# acme update\n")},
|
|
"acme-docx/references/lark-doc-xml.md": &fstest.MapFile{Data: []byte("# acme xml\n")},
|
|
}
|
|
platform.Register(
|
|
platform.NewPlugin("replacement-skills", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{
|
|
Base: base,
|
|
ReferenceRemaps: []platform.SkillRefRemap{
|
|
platform.RemapSkillRef("lark-doc", "acme-docx"),
|
|
platform.RemapSkillRef(
|
|
"lark-doc/references/lark-doc-create.md",
|
|
"acme-docx/guides/create.md",
|
|
),
|
|
},
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
const removeDocsSkillPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import "github.com/larksuite/cli/extension/platform"
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("remove-docs-skill", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{
|
|
Remove: []string{"lark-doc"},
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
const concealSkillsCommandPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import (
|
|
"testing/fstest"
|
|
|
|
"github.com/larksuite/cli/extension/platform"
|
|
)
|
|
|
|
func init() {
|
|
platform.Register(
|
|
platform.NewPlugin("conceal-skills-command", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{
|
|
Overlay: fstest.MapFS{
|
|
"lark-im/SKILL.md": &fstest.MapFile{Data: []byte("---\nname: lark-im\ndescription: IM guide\n---\n")},
|
|
},
|
|
}).
|
|
Restrict(&platform.Rule{
|
|
Deny: []string{"skills", "skills/**"},
|
|
MaxRisk: platform.RiskHighRiskWrite,
|
|
AllowUnannotated: true,
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
const runtimePanicSkillFSPlugin = `// Code generated by plugin_e2e; DO NOT EDIT.
|
|
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"testing/fstest"
|
|
|
|
"github.com/larksuite/cli/extension/platform"
|
|
)
|
|
|
|
var panicOnRead bool
|
|
|
|
type latePanicFS struct{ fs.FS }
|
|
|
|
func (f latePanicFS) ReadFile(name string) ([]byte, error) {
|
|
if panicOnRead {
|
|
panic("runtime skill read exploded")
|
|
}
|
|
return fs.ReadFile(f.FS, name)
|
|
}
|
|
|
|
func init() {
|
|
content := fstest.MapFS{
|
|
"acme-guide/SKILL.md": &fstest.MapFile{Data: []byte("---\nname: acme-guide\ndescription: panic boundary\n---\n")},
|
|
"acme-guide/references/x.md": &fstest.MapFile{Data: []byte("# x\n")},
|
|
}
|
|
platform.Register(
|
|
platform.NewPlugin("runtime-panic-fs", "0.1.0").
|
|
EmbeddedSkills(&platform.SkillsOverlay{Base: latePanicFS{FS: content}}).
|
|
On(platform.Startup, "arm-panic",
|
|
func(context.Context, *platform.LifecycleContext) error {
|
|
panicOnRead = true
|
|
return nil
|
|
}).
|
|
MustBuild())
|
|
}
|
|
`
|
|
|
|
func TestForkMainWiresEmbeddedSkillsWithoutOverlay(t *testing.T) {
|
|
bin := buildFork(t, "smoke", noopPlugin)
|
|
t.Setenv("PATH", filepath.Dir(bin)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
const cli = "lark-cli"
|
|
|
|
res := run(t, cli, "skills", "list")
|
|
if res.exit != 0 || !gjson.Valid(res.stdout) {
|
|
t.Fatalf("skills list exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
}
|
|
if got := gjson.Get(res.stdout, "count").Int(); got != 4 {
|
|
t.Fatalf("skills count=%d want 4; stdout=%s", got, res.stdout)
|
|
}
|
|
if got := gjson.Get(res.stdout, "skills.#.name").Array(); len(got) != 4 ||
|
|
got[0].String() != "lark-a" || got[1].String() != "lark-b" ||
|
|
got[2].String() != "lark-doc" || got[3].String() != "lark-shared" {
|
|
t.Fatalf("skill names=%v want [lark-a lark-b lark-doc lark-shared]; stdout=%s", got, res.stdout)
|
|
}
|
|
|
|
docsHelp := run(t, cli, "docs", "--help")
|
|
if docsHelp.exit != 0 ||
|
|
!strings.Contains(docsHelp.stdout,
|
|
"Domain guide (concepts, command choice, conventions): lark-cli skills read lark-doc") ||
|
|
strings.Contains(docsHelp.stdout, "Domain skills (concepts, command choice, conventions):") {
|
|
t.Fatalf("docs help canonical skill pointer missing: exit=%d stdout=%s stderr=%s",
|
|
docsHelp.exit, docsHelp.stdout, docsHelp.stderr)
|
|
}
|
|
mainSkill := run(t, cli, "skills", "read", "lark-doc")
|
|
if mainSkill.exit != 0 || !strings.Contains(mainSkill.stdout, "name: lark-doc") {
|
|
t.Fatalf("canonical docs skill pointer is not readable: exit=%d stdout=%s stderr=%s",
|
|
mainSkill.exit, mainSkill.stdout, mainSkill.stderr)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
shortcut string
|
|
references []string
|
|
}{
|
|
{shortcut: "+create", references: []string{"lark-doc-create.md", "lark-doc-xml.md", "lark-doc-md.md"}},
|
|
{shortcut: "+fetch", references: []string{"lark-doc-fetch.md"}},
|
|
{shortcut: "+update", references: []string{"lark-doc-update.md", "lark-doc-xml.md", "lark-doc-md.md"}},
|
|
{shortcut: "+history-list", references: []string{"lark-doc-history.md"}},
|
|
{shortcut: "+history-revert", references: []string{"lark-doc-history.md"}},
|
|
{shortcut: "+history-revert-status", references: []string{"lark-doc-history.md"}},
|
|
} {
|
|
help := run(t, cli, "docs", tc.shortcut, "--help")
|
|
if help.exit != 0 {
|
|
t.Errorf("docs %s help failed: exit=%d stdout=%s stderr=%s",
|
|
tc.shortcut, help.exit, help.stdout, help.stderr)
|
|
continue
|
|
}
|
|
for _, reference := range tc.references {
|
|
path := "references/" + reference
|
|
pointer := "lark-cli skills read lark-doc/" + path
|
|
if !strings.Contains(help.stdout, pointer) {
|
|
t.Errorf("docs %s reference pointer %q missing; stdout=%s",
|
|
tc.shortcut, pointer, help.stdout)
|
|
continue
|
|
}
|
|
// Execute the exact binary name and arguments printed by help:
|
|
// PATH resolution here catches a fork shipped only as "larkx".
|
|
rules := run(t, cli, "skills", "read", "lark-doc/"+path)
|
|
if rules.exit != 0 || !strings.Contains(rules.stdout, "Plugin E2E "+reference) {
|
|
t.Errorf("docs %s reference pointer %q is not readable: exit=%d stdout=%s stderr=%s",
|
|
tc.shortcut, pointer, rules.exit, rules.stdout, rules.stderr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestForkDomainHelpDisplaysConfiguredSkills(t *testing.T) {
|
|
affordanceSrc := strings.Replace(
|
|
customerAffordanceDocs,
|
|
"> skill: lark-doc\n",
|
|
"> skill: lark-doc\n\n## Skills\n- lark-a\n- lark-missing\n",
|
|
1,
|
|
)
|
|
bin := buildForkWithAffordance(t, "domain-skills", noopPlugin, affordanceSrc)
|
|
|
|
help := run(t, bin, "docs", "--help")
|
|
if help.exit != 0 {
|
|
t.Fatalf("docs help: exit=%d stdout=%s stderr=%s", help.exit, help.stdout, help.stderr)
|
|
}
|
|
if !strings.Contains(help.stdout, "Domain skills (concepts, command choice, conventions):") {
|
|
t.Fatalf("configured domain skills did not render as a list:\n%s", help.stdout)
|
|
}
|
|
docAt := strings.Index(help.stdout, "lark-cli skills read lark-doc")
|
|
aAt := strings.Index(help.stdout, "lark-cli skills read lark-a")
|
|
if docAt < 0 || aAt < 0 || docAt >= aAt {
|
|
t.Fatalf("domain skill pointers missing or out of order (doc=%d a=%d):\n%s", docAt, aAt, help.stdout)
|
|
}
|
|
if strings.Contains(help.stdout, "lark-missing") {
|
|
t.Fatalf("unreadable domain skill pointer was not filtered:\n%s", help.stdout)
|
|
}
|
|
for _, skill := range []string{"lark-doc", "lark-a"} {
|
|
read := run(t, bin, "skills", "read", skill)
|
|
if read.exit != 0 || !strings.Contains(read.stdout, "name: "+skill) {
|
|
t.Errorf("rendered domain skill %q is not readable: exit=%d stdout=%s stderr=%s",
|
|
skill, read.exit, read.stdout, read.stderr)
|
|
}
|
|
}
|
|
|
|
commandHelp := run(t, bin, "docs", "+create", "--help")
|
|
if commandHelp.exit != 0 {
|
|
t.Fatalf("docs +create help: exit=%d stdout=%s stderr=%s",
|
|
commandHelp.exit, commandHelp.stdout, commandHelp.stderr)
|
|
}
|
|
if strings.Contains(commandHelp.stdout, "lark-cli skills read lark-a") {
|
|
t.Fatalf("domain-only skill leaked into command help:\n%s", commandHelp.stdout)
|
|
}
|
|
}
|
|
|
|
func TestForkSkillsAllowRemoveComposeAgainstEmbeddedBase(t *testing.T) {
|
|
bin := buildFork(t, "skill-selection", skillSelectionPlugin)
|
|
res := run(t, bin, "skills", "list")
|
|
if res.exit != 0 || !gjson.Valid(res.stdout) {
|
|
t.Fatalf("skills list exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
}
|
|
if got := gjson.Get(res.stdout, "count").Int(); got != 1 {
|
|
t.Fatalf("skills count=%d want 1; stdout=%s", got, res.stdout)
|
|
}
|
|
if got := gjson.Get(res.stdout, "skills.0.name").String(); got != "lark-a" {
|
|
t.Fatalf("skill name=%q want lark-a; stdout=%s", got, res.stdout)
|
|
}
|
|
|
|
// Pin that the invalid-overlay fatal guard was not installed globally.
|
|
help := run(t, bin, "im", "--help")
|
|
if help.exit != 0 {
|
|
t.Fatalf("unrelated command was bricked: exit=%d stdout=%s stderr=%s", help.exit, help.stdout, help.stderr)
|
|
}
|
|
}
|
|
|
|
func TestForkSkillsMissingRequiredSkillUsesTypedStartupGuard(t *testing.T) {
|
|
bin := buildFork(t, "incomplete-skill-dependency", incompleteSkillDependencyPlugin)
|
|
res := run(t, bin, "skills", "list")
|
|
if res.exit != 2 || !gjson.Valid(res.stderr) {
|
|
t.Fatalf("skills list exit=%d stdout=%s stderr=%s", res.exit, res.stdout, res.stderr)
|
|
}
|
|
if got := gjson.Get(res.stderr, "error.type").String(); got != "validation" {
|
|
t.Errorf("error.type=%q want validation; stderr=%s", got, res.stderr)
|
|
}
|
|
if got := gjson.Get(res.stderr, "error.subtype").String(); got != "failed_precondition" {
|
|
t.Errorf("error.subtype=%q want failed_precondition; stderr=%s", got, res.stderr)
|
|
}
|
|
hint := gjson.Get(res.stderr, "error.hint").String()
|
|
reasonMatches := regexp.MustCompile(`\breason_code ([a-z0-9_]+)\b`).FindAllStringSubmatch(hint, -1)
|
|
if len(reasonMatches) != 1 || reasonMatches[0][1] != "invalid_skills_overlay" {
|
|
t.Errorf("error.hint reason_codes=%v want exactly [invalid_skills_overlay]; hint=%q", reasonMatches, hint)
|
|
}
|
|
|
|
message := gjson.Get(res.stderr, "error.message").String()
|
|
affectedSkills := make(map[string]struct{})
|
|
for _, match := range regexp.MustCompile(`\bskill "([^"]+)"`).FindAllStringSubmatch(message, -1) {
|
|
affectedSkills[match[1]] = struct{}{}
|
|
}
|
|
if len(affectedSkills) != 2 {
|
|
t.Errorf("affected skills=%v want exactly [lark-doc lark-shared]; message=%q", affectedSkills, message)
|
|
} else {
|
|
for _, want := range []string{"lark-doc", "lark-shared"} {
|
|
if _, ok := affectedSkills[want]; !ok {
|
|
t.Errorf("affected skills=%v missing %q; message=%q", affectedSkills, want, message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestForkSkillsIncludingRequiredSkillSucceeds(t *testing.T) {
|
|
bin := buildFork(t, "complete-skill-dependency", completeSkillDependencyPlugin)
|
|
list := run(t, bin, "skills", "list")
|
|
if list.exit != 0 || !gjson.Valid(list.stdout) {
|
|
t.Fatalf("skills list exit=%d stdout=%s stderr=%s", list.exit, list.stdout, list.stderr)
|
|
}
|
|
if got := gjson.Get(list.stdout, "skills.#.name").Array(); len(got) != 2 ||
|
|
got[0].String() != "lark-doc" || got[1].String() != "lark-shared" {
|
|
t.Fatalf("skill names=%v want [lark-doc lark-shared]; stdout=%s", got, list.stdout)
|
|
}
|
|
for _, name := range []string{"lark-doc", "lark-shared"} {
|
|
read := run(t, bin, "skills", "read", name)
|
|
if read.exit != 0 || !strings.Contains(read.stdout, "name: "+name) {
|
|
t.Errorf("skill %q unreadable: exit=%d stdout=%s stderr=%s", name, read.exit, read.stdout, read.stderr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestForkSkillsBaseReplacementAndReferenceRemapWithoutHostBase(t *testing.T) {
|
|
bin := buildForkWithMain(
|
|
t,
|
|
"replacement-skill-tree",
|
|
replacementSkillTreePlugin,
|
|
customerMainWithoutSkills,
|
|
)
|
|
|
|
list := run(t, bin, "skills", "list")
|
|
if list.exit != 0 || gjson.Get(list.stdout, "count").Int() != 1 ||
|
|
gjson.Get(list.stdout, "skills.0.name").String() != "acme-docx" {
|
|
t.Fatalf("replacement skills list: exit=%d stdout=%s stderr=%s",
|
|
list.exit, list.stdout, list.stderr)
|
|
}
|
|
|
|
help := run(t, bin, "docs", "+create", "--help")
|
|
if help.exit != 0 {
|
|
t.Fatalf("remapped docs help: exit=%d stdout=%s stderr=%s",
|
|
help.exit, help.stdout, help.stderr)
|
|
}
|
|
for _, want := range []string{
|
|
"lark-cli skills read acme-docx/guides/create.md",
|
|
"lark-cli skills read acme-docx/references/lark-doc-xml.md",
|
|
"lark-cli skills read acme-docx/references/lark-doc-md.md",
|
|
} {
|
|
if !strings.Contains(help.stdout, want) {
|
|
t.Errorf("remapped docs help missing %q:\n%s", want, help.stdout)
|
|
}
|
|
}
|
|
if strings.Contains(help.stdout, "lark-cli skills read lark-doc") {
|
|
t.Errorf("remapped docs help leaked canonical runtime name:\n%s", help.stdout)
|
|
}
|
|
|
|
read := run(t, bin, "skills", "read", "acme-docx/guides/create.md")
|
|
if read.exit != 0 || !strings.Contains(read.stdout, "# acme create") {
|
|
t.Fatalf("remapped pointer unreadable: exit=%d stdout=%s stderr=%s",
|
|
read.exit, read.stdout, read.stderr)
|
|
}
|
|
|
|
configDir := t.TempDir()
|
|
writeFile(t, filepath.Join(configDir, "config.json"),
|
|
`{"apps":[{"appId":"cli_plugin_e2e","appSecret":"secret","brand":"feishu","users":[]}]}`)
|
|
legacyEnv := append(baseEnv(),
|
|
"LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1",
|
|
"LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1",
|
|
"LARKSUITE_CLI_CONFIG_DIR="+configDir,
|
|
"LARKSUITE_CLI_REMOTE_META=off",
|
|
)
|
|
legacy := runWithEnv(t, bin, legacyEnv,
|
|
"docs", "+update", "--doc", "doccn-plugin-e2e", "--mode", "replace", "--as", "user")
|
|
if legacy.exit != 2 || !gjson.Valid(legacy.stderr) {
|
|
t.Fatalf("remapped v2-only error: exit=%d stdout=%s stderr=%s",
|
|
legacy.exit, legacy.stdout, legacy.stderr)
|
|
}
|
|
if got := gjson.Get(legacy.stderr, "error.subtype").String(); got != "invalid_argument" {
|
|
t.Fatalf("remapped v2-only subtype=%q want invalid_argument; stderr=%s",
|
|
got, legacy.stderr)
|
|
}
|
|
if got := gjson.Get(legacy.stderr, "error.param").String(); got != "--mode" {
|
|
t.Fatalf("remapped v2-only param=%q want --mode; stderr=%s",
|
|
got, legacy.stderr)
|
|
}
|
|
hint := gjson.Get(legacy.stderr, "error.hint").String()
|
|
for _, want := range []string{
|
|
"`lark-cli skills read acme-docx`",
|
|
"`lark-cli skills read acme-docx/references/lark-doc-update.md`",
|
|
"`lark-cli skills read acme-docx/references/lark-doc-xml.md`",
|
|
"`lark-cli skills read acme-docx/references/lark-doc-md.md`",
|
|
} {
|
|
if !strings.Contains(hint, want) {
|
|
t.Errorf("remapped v2-only hint missing %q: %q", want, hint)
|
|
}
|
|
}
|
|
if strings.Contains(hint, "lark-cli skills read lark-doc") {
|
|
t.Errorf("remapped v2-only hint leaked canonical skill reference: %q", hint)
|
|
}
|
|
}
|
|
|
|
func TestForkRemovingDocsSkillDropsPointersButKeepsStandaloneGuidance(t *testing.T) {
|
|
bin := buildFork(t, "remove-docs-skill", removeDocsSkillPlugin)
|
|
for _, args := range [][]string{
|
|
{"docs", "--help"},
|
|
{"docs", "+create", "--help"},
|
|
} {
|
|
help := run(t, bin, args...)
|
|
if help.exit != 0 {
|
|
t.Fatalf("%v: exit=%d stdout=%s stderr=%s", args, help.exit, help.stdout, help.stderr)
|
|
}
|
|
for _, dead := range []string{"skills read", "AI agents MUST read"} {
|
|
if strings.Contains(help.stdout, dead) {
|
|
t.Errorf("%v retained removed-skill guidance %q:\n%s", args, dead, help.stdout)
|
|
}
|
|
}
|
|
if len(args) > 1 && args[1] == "+create" {
|
|
for _, want := range []string{"Tips:", "Match --doc-format", "Prefer @file"} {
|
|
if !strings.Contains(help.stdout, want) {
|
|
t.Errorf("%v lost standalone help guidance %q after skill removal:\n%s", args, want, help.stdout)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
legacy := run(t, bin, "docs", "+update", "--mode", "replace")
|
|
if legacy.exit != 2 {
|
|
t.Fatalf("legacy v2-only probe: exit=%d stdout=%s stderr=%s",
|
|
legacy.exit, legacy.stdout, legacy.stderr)
|
|
}
|
|
if strings.Contains(legacy.stderr, "skills read") {
|
|
t.Errorf("v2-only error retained removed-skill pointer: %s", legacy.stderr)
|
|
}
|
|
}
|
|
|
|
func TestForkConcealingSkillsCommandDropsDocsPointersEvenWhenContentExists(t *testing.T) {
|
|
bin := buildConcealedFork(t, "concealed-skills-command", concealSkillsCommandPlugin)
|
|
|
|
help := run(t, bin, "docs", "+create", "--help")
|
|
if help.exit != 0 {
|
|
t.Fatalf("docs help: exit=%d stdout=%s stderr=%s", help.exit, help.stdout, help.stderr)
|
|
}
|
|
if strings.Contains(help.stdout, "skills read") {
|
|
t.Errorf("docs help points at concealed skills/read:\n%s", help.stdout)
|
|
}
|
|
|
|
imHelp := run(t, bin, "im", "--help")
|
|
if imHelp.exit != 0 {
|
|
t.Fatalf("im help: exit=%d stdout=%s stderr=%s",
|
|
imHelp.exit, imHelp.stdout, imHelp.stderr)
|
|
}
|
|
if strings.Contains(imHelp.stdout, "skills read") {
|
|
t.Errorf("generic domain help points at concealed skills/read:\n%s", imHelp.stdout)
|
|
}
|
|
|
|
legacy := run(t, bin, "docs", "+update", "--mode", "replace")
|
|
if legacy.exit != 2 {
|
|
t.Fatalf("legacy v2-only probe: exit=%d stdout=%s stderr=%s",
|
|
legacy.exit, legacy.stdout, legacy.stderr)
|
|
}
|
|
if strings.Contains(legacy.stderr, "skills read") {
|
|
t.Errorf("v2-only error points at concealed skills/read: %s", legacy.stderr)
|
|
}
|
|
}
|
|
|
|
func TestForkPluginFSPanicBecomesTypedRuntimeError(t *testing.T) {
|
|
bin := buildForkWithMain(
|
|
t,
|
|
"runtime-panic-skill-fs",
|
|
runtimePanicSkillFSPlugin,
|
|
customerMainWithoutSkills,
|
|
)
|
|
res := run(t, bin, "skills", "read", "acme-guide/references/x.md")
|
|
if res.exit != 5 || !gjson.Valid(res.stderr) {
|
|
t.Fatalf("runtime plugin FS panic exit=%d stdout=%s stderr=%s",
|
|
res.exit, res.stdout, res.stderr)
|
|
}
|
|
if got := gjson.Get(res.stderr, "error.type").String(); got != "internal" {
|
|
t.Errorf("error.type=%q want internal; stderr=%s", got, res.stderr)
|
|
}
|
|
for _, want := range []string{
|
|
`plugin \"runtime-panic-fs\"`,
|
|
"Base filesystem",
|
|
"readfile",
|
|
"acme-guide/references/x.md",
|
|
} {
|
|
if !strings.Contains(res.stderr, want) {
|
|
t.Errorf("stderr missing %q: %s", want, res.stderr)
|
|
}
|
|
}
|
|
if strings.Contains(res.stderr, "goroutine ") {
|
|
t.Errorf("runtime plugin FS panic escaped as a stack trace: %s", res.stderr)
|
|
}
|
|
}
|