Files
tutti/packages/agent/runtimeprep/provider_instruction_file_test.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

222 lines
8.0 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.
package runtimeprep
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestDefaultPreparerACPExtensionMaterializesSkillsToDeclaredRoot 守住 agent
// extension 的 skill 物化:acp: 前缀的 provider 走通用 InstructionFilePreparer,
// tutti 内置 skill 物化到 extension composer profile 声明的 skill root
// (ExtensionSkillRoots),而非硬编码的 providerSkillRoot。修复前 acp:hermes 在
// DefaultPreparer.provider() 查不到 preparer,整个 provider prepare 被跳过,
// AGENTS.md 与 skill 均不物化。
func TestDefaultPreparerACPExtensionMaterializesSkillsToDeclaredRoot(t *testing.T) {
stateDir := t.TempDir()
cwd := t.TempDir()
prep := NewDefaultPreparer(stateDir)
prep.CommandCatalog = staticCommandCatalog(nil)
prepared, err := prep.Prepare(t.Context(), PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "local:extension-test",
Provider: "acp:extension-test",
Cwd: cwd,
ExtensionSkillRoots: []string{".agent_context/skills"},
})
if err != nil {
t.Fatalf("Prepare() error = %v", err)
}
_ = prepared
// 通用 InstructionFilePreparer 写 AGENTS.md 到 cwd(provider-instructions)。
if _, err := os.Stat(filepath.Join(cwd, "AGENTS.md")); err != nil {
t.Fatalf("cwd AGENTS.md missing (provider-instructions): %v", err)
}
// tutti 内置 skill 物化到 extension 声明的 root(.agent_context/skills)。
for _, name := range []string{tuttiHandoffSkillName, tuttiSkillName} {
skillPath := filepath.Join(cwd, ".agent_context", "skills", name, "SKILL.md")
if _, err := os.Stat(skillPath); err != nil {
t.Fatalf("skill %s SKILL.md missing at declared root: %v", name, err)
}
}
}
func TestDefaultPreparerKimiCodeMaterializesCanonicalSkillBundle(t *testing.T) {
t.Setenv(browserUseSwitchEnv, "")
stateDir := t.TempDir()
cwd := t.TempDir()
prep := newTestPreparer(stateDir)
prep.CLICommand = "tutti-dev"
input := PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "extension:kimi-code",
Provider: "acp:kimi-code",
Cwd: cwd,
BrowserUse: true,
ExtensionSkillRoots: []string{".agents/skills"},
}
if _, err := prep.Prepare(t.Context(), input); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
materialized, err := os.ReadFile(filepath.Join(
cwd,
".agents",
"skills",
browserUseSkillName,
"SKILL.md",
))
if err != nil {
t.Fatalf("read materialized Kimi browser-use skill: %v", err)
}
bundle, err := prep.RenderSkillBundle(t.Context(), input)
if err != nil {
t.Fatalf("RenderSkillBundle() error = %v", err)
}
canonical := skillBundleRecord(bundle.Skills, browserUseSkillName).Content
if canonical == "" {
t.Fatal("canonical browser-use skill is missing from rendered bundle")
}
if string(materialized) != canonical {
t.Fatalf("materialized Kimi skill diverged from canonical bundle:\n%s", materialized)
}
if !strings.Contains(canonical, "tutti-dev browser open --url <url> --json") ||
strings.Contains(canonical, "{{") {
t.Fatalf("canonical Kimi skill was not fully rendered:\n%s", canonical)
}
}
func TestDefaultPreparerACPExtensionReplacesManagedSkillsWithoutSuffixes(t *testing.T) {
stateDir := t.TempDir()
cwd := t.TempDir()
prep := NewDefaultPreparer(stateDir)
prep.CommandCatalog = staticCommandCatalog(nil)
input := PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "local:extension-test",
Provider: "acp:extension-test",
Cwd: cwd,
ExtensionSkillRoots: []string{".agent_context/skills"},
}
for range 2 {
if _, err := prep.Prepare(t.Context(), input); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
}
entries, err := os.ReadDir(filepath.Join(cwd, ".agent_context", "skills"))
if err != nil {
t.Fatalf("ReadDir() error = %v", err)
}
for _, skillName := range []string{tuttiHandoffSkillName, tuttiSkillName} {
matches := 0
for _, entry := range entries {
if entry.IsDir() && (entry.Name() == skillName || strings.HasPrefix(entry.Name(), skillName+"-")) {
matches++
}
}
if matches != 1 {
t.Fatalf("managed skill %q directory count = %d, want 1", skillName, matches)
}
}
}
func TestDefaultPreparerACPExtensionUsesStableSuffixWhenUserDirsCollide(t *testing.T) {
stateDir := t.TempDir()
cwd := t.TempDir()
root := filepath.Join(cwd, ".agent_context", "skills")
for _, name := range []string{tuttiSkillName, tuttiSkillName + "-tutti"} {
if err := os.MkdirAll(filepath.Join(root, name), 0o755); err != nil {
t.Fatalf("create user skill dir: %v", err)
}
}
prep := NewDefaultPreparer(stateDir)
prep.CommandCatalog = staticCommandCatalog(nil)
if _, err := prep.Prepare(t.Context(), PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "local:extension-test",
Provider: "acp:extension-test",
Cwd: cwd,
ExtensionSkillRoots: []string{".agent_context/skills"},
}); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
if _, err := os.Stat(filepath.Join(root, tuttiSkillName+"-tutti-2", "SKILL.md")); err != nil {
t.Fatalf("managed skill should fall back to stable suffix: %v", err)
}
}
func TestDefaultPreparerACPExtensionRejectsUnsafeSkillRoot(t *testing.T) {
stateDir := t.TempDir()
cwd := t.TempDir()
prep := NewDefaultPreparer(stateDir)
prep.CommandCatalog = staticCommandCatalog(nil)
if _, err := prep.Prepare(t.Context(), PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "local:extension-test",
Provider: "acp:extension-test",
Cwd: cwd,
ExtensionSkillRoots: []string{"../outside"},
}); err == nil {
t.Fatal("Prepare() should reject unsafe extension skill root")
}
}
func TestExtensionRuntimePreparerWithoutHomeRejectsUnsafeSkillRoot(t *testing.T) {
stateDir := t.TempDir()
cwd := t.TempDir()
prep := NewDefaultPreparer(stateDir)
prep.CommandCatalog = staticCommandCatalog(nil)
if _, err := prep.Prepare(t.Context(), PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "local:extension-test",
Provider: "acp:extension-test",
Cwd: cwd,
ExtensionRuntimePrep: &ExtensionRuntimePrep{InstructionsFile: "AGENTS.md"},
ExtensionSkillRoots: []string{"/tmp/outside"},
}); err == nil {
t.Fatal("Prepare() should reject unsafe extension runtime skill root")
}
}
// TestDefaultPreparerACPExtensionWithoutSkillRootsSkipsSkillMaterialization
// 确认未声明 skill root 的 acp: extension 仍写 AGENTS.md,但不物化 skill
// (providerSkillRoot 对 acp: 前缀返回空,ExtensionSkillRoots 也空)。
func TestDefaultPreparerACPExtensionWithoutSkillRootsSkipsSkillMaterialization(t *testing.T) {
stateDir := t.TempDir()
cwd := t.TempDir()
prep := NewDefaultPreparer(stateDir)
prep.CommandCatalog = staticCommandCatalog(nil)
if _, err := prep.Prepare(t.Context(), PrepareInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "local:extension-test",
Provider: "acp:extension-test",
Cwd: cwd,
}); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
if _, err := os.Stat(filepath.Join(cwd, ".agent_context", "skills")); !os.IsNotExist(err) {
t.Fatalf(".agent_context/skills should not exist, got err=%v", err)
}
if _, err := os.Stat(filepath.Join(cwd, "AGENTS.md")); err != nil {
t.Fatalf("cwd AGENTS.md missing (instructions should still be written): %v", err)
}
}
// TestACPExtensionExecutionEnvCoveredByTemplate 确认 acp: prefix 的 extension provider
// 在重构后的模板系统中通过 provider-execution.md 通用兜底(line 8)获得 localhost/IPC
// 提示。旧的 providerSpecificExecutionEnvironment 已被模板系统替代,不再需要单独测试。
func TestACPExtensionExecutionEnvCoveredByTemplate(t *testing.T) {
t.Log("acp: extension execution env now covered by provider-execution.md generic fallback template")
}