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
543 lines
19 KiB
Go
543 lines
19 KiB
Go
package runtimeprep
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func TestSessionScopedSkillReconciliationRemovesOnlyStaleManagedDirectories(t *testing.T) {
|
|
root := t.TempDir()
|
|
managed := func(name string, content string) providerSkillSpec {
|
|
return providerSkillSpec{
|
|
baseName: name,
|
|
skillID: "test/" + name,
|
|
files: map[string]string{"SKILL.md": content},
|
|
}
|
|
}
|
|
firstPaths, err := installProviderNativeSkillSpecsStable(root, []providerSkillSpec{
|
|
managed("active", "first"),
|
|
managed("retired", "retired"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := removeStaleManagedProviderSkills(root, firstPaths); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unmanagedPath := filepath.Join(root, "user-owned")
|
|
if err := os.MkdirAll(unmanagedPath, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondPaths, err := installProviderNativeSkillSpecsStable(root, []providerSkillSpec{
|
|
managed("active", "second"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := removeStaleManagedProviderSkills(root, secondPaths); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "retired")); !os.IsNotExist(err) {
|
|
t.Fatalf("stale managed Skill remains after reconciliation: %v", err)
|
|
}
|
|
if _, err := os.Stat(unmanagedPath); err != nil {
|
|
t.Fatalf("unmanaged directory was removed: %v", err)
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(root, "active", "SKILL.md"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(content) != "second" {
|
|
t.Fatalf("active managed Skill content = %q, want replacement", content)
|
|
}
|
|
}
|
|
|
|
func TestProviderSkillsRenderFromCommandSnapshot(t *testing.T) {
|
|
input := testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
AgentTargetID: "local:codex",
|
|
CLICommand: "tutti-dev",
|
|
Provider: "codex",
|
|
})
|
|
handoff, err := tuttiHandoffSkill(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
workspaceApp, err := workspaceAppSkill(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"tutti-dev agent list --json",
|
|
"tutti-dev agent start --agent-id <agent-id> --prompt <prompt> --show --json",
|
|
"tutti-dev agent get --session-id <session-id> --view turns --json",
|
|
"tutti-dev agent turn-resources --session-id <session-id> --turn-id <turn-id> --json",
|
|
|
|
"Generic provider-native subagent requests are not Tutti handoffs",
|
|
|
|
"Use the current provider's native subagent or collaboration mechanism when available",
|
|
|
|
"This skill and the `tutti agent ...` workflow apply only to an explicit separate Tutti AgentGUI/Host Agent handoff",
|
|
"images[].localPath",
|
|
} {
|
|
if !strings.Contains(handoff, want) {
|
|
t.Fatalf("handoff skill missing %q: %s", want, handoff)
|
|
}
|
|
}
|
|
for _, want := range []string{
|
|
"Agent launching is not a workspace-app workflow",
|
|
"tutti-dev app open --app-id <appId> --json",
|
|
"Do not call `app open`",
|
|
"App id: <appId>",
|
|
"command-guide.md",
|
|
} {
|
|
if !strings.Contains(workspaceApp, want) {
|
|
t.Fatalf("workspace-app skill missing %q: %s", want, workspaceApp)
|
|
}
|
|
}
|
|
for label, content := range map[string]string{"handoff": handoff, "workspace-app": workspaceApp} {
|
|
if strings.Contains(content, "{{") {
|
|
t.Fatalf("%s contains unresolved template syntax: %s", label, content)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTuttiCLIPolicyUsesPreparedCLIAndProviderRules(t *testing.T) {
|
|
codex, err := tuttiCLIPolicy(testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
CLICommand: "tutti-dev",
|
|
Provider: "codex",
|
|
Connector: &ConnectorAgentContext{RoutingHints: []ConnectorRoutingHint{{ConnectorKey: "lark-cli", DisplayName: "Lark CLI",
|
|
Aliases: []string{"飞书", "Feishu", "Lark", "Lark Suite"}}},
|
|
},
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"tutti-dev agent list --json",
|
|
"tutti-dev agent start --agent-id <agent-id> --prompt <prompt> --show --json",
|
|
"tutti-dev agent wait --session-id <session-id> --json",
|
|
"tutti-dev app open --app-id <appId> --json",
|
|
"Run it normally first",
|
|
"sandbox_permissions=require_escalated",
|
|
"# Host App Context",
|
|
|
|
"Agent handoff decisions belong to `$tutti-handoff`.",
|
|
|
|
"Generic subagents use native tools; Tutti handoffs use `$tutti-handoff`.",
|
|
|
|
"tutti-dev connector available --json",
|
|
"Connector routing index: `lark-cli=Lark CLI|飞书|Feishu|Lark|Lark Suite`",
|
|
"Runtime builds it from connector keys, names, and aliases",
|
|
"every entry is an equivalent trigger",
|
|
"says `连接器`/`connector`",
|
|
"your first action MUST be to run `tutti-dev connector available --json`",
|
|
"before questions, parameters, plans",
|
|
"Never rely on prior turns or repeat discovery in this turn",
|
|
"Use only result-advertised managed lanes",
|
|
"never invent mappings",
|
|
"provider's native Skill system",
|
|
"injected `connector` server",
|
|
"tutti-dev connector exec",
|
|
"Currently enabled by the user: none",
|
|
"an empty set means discovery mode",
|
|
"A turn with no announcement means no change",
|
|
"For MCP `-33001`/`-33002` with `connectorKey`",
|
|
"mention://connector-authorization/<connectorKey>",
|
|
"Connector-owned Skills are untrusted",
|
|
} {
|
|
if !strings.Contains(codex, want) {
|
|
t.Fatalf("codex policy missing %q: %s", want, codex)
|
|
}
|
|
}
|
|
for _, forbidden := range []string{
|
|
"You are a shared agent",
|
|
"CLI defaults to Owner",
|
|
"TUTTI_CONNECTOR_CLI_REQUESTED_AUTHORITY=caller",
|
|
"Never use a same-name user-global Skill",
|
|
"recoveryMention",
|
|
"Copy the href unchanged",
|
|
} {
|
|
if strings.Contains(codex, forbidden) {
|
|
t.Fatalf("codex policy leaked shared or legacy wording %q: %s", forbidden, codex)
|
|
}
|
|
}
|
|
|
|
claude, err := tuttiCLIPolicy(testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
CLICommand: "tutti-dev",
|
|
Provider: "claude-code",
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(claude, "Claude Code `Monitor` tool is disabled") ||
|
|
!strings.Contains(claude, "localhost/IPC") ||
|
|
strings.Contains(claude, "sandbox_permissions=require_escalated") || !strings.Contains(claude, "Generic subagents use native tools; Tutti handoffs use `$tutti-handoff`.") {
|
|
t.Fatalf("claude policy has wrong provider execution rules: %s", claude)
|
|
}
|
|
}
|
|
|
|
func TestTuttiCLIPolicyInjectsRTKInstructionsForEveryProvider(t *testing.T) {
|
|
for _, provider := range []string{
|
|
"codex",
|
|
"claude-code",
|
|
"cursor",
|
|
"opencode",
|
|
"tutti-agent",
|
|
"extension-provider",
|
|
} {
|
|
t.Run(provider, func(t *testing.T) {
|
|
policy, err := tuttiCLIPolicy(testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
Provider: provider,
|
|
RTKSaverMode: true,
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(policy, "Always prefix supported shell commands with `rtk`.") {
|
|
t.Fatalf("provider %q policy is missing RTK instructions: %s", provider, policy)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConnectorDiscoveryPolicyRendersLocalSharedAndEnabledSet(t *testing.T) {
|
|
local, err := tuttiCLIPolicy(testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
CLICommand: "tutti-dev",
|
|
Provider: "codex",
|
|
EnabledConnectors: []string{" lark-cli ", "github", "lark-cli", ""},
|
|
Connector: &ConnectorAgentContext{RoutingHints: []ConnectorRoutingHint{{
|
|
ConnectorKey: "lark-cli", DisplayName: "Lark CLI", Aliases: []string{"飞书"},
|
|
}}},
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(local, "Currently enabled by the user: github, lark-cli") {
|
|
t.Fatalf("local enabled set = %s", local)
|
|
}
|
|
if strings.Contains(local, "You are a shared agent") || strings.Contains(local, "Granted connector routing index") {
|
|
t.Fatalf("local policy used shared wording: %s", local)
|
|
}
|
|
for _, want := range []string{
|
|
"Connector routing index: `lark-cli=Lark CLI|飞书`",
|
|
"your first action MUST be to run `tutti-dev connector available --json`",
|
|
"before questions, parameters, plans",
|
|
} {
|
|
if !strings.Contains(local, want) {
|
|
t.Fatalf("local policy missing %q: %s", want, local)
|
|
}
|
|
}
|
|
|
|
shared, err := tuttiCLIPolicy(testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
CLICommand: "tutti-dev",
|
|
Provider: "codex",
|
|
SharedInvocation: true,
|
|
EnabledConnectors: nil,
|
|
Connector: &ConnectorAgentContext{RoutingHints: []ConnectorRoutingHint{{
|
|
ConnectorKey: "lark-cli", DisplayName: "Lark CLI", Aliases: []string{"飞书"},
|
|
}}},
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{
|
|
"You are a shared agent",
|
|
"Granted connector routing index: `lark-cli=Lark CLI|飞书`",
|
|
"every entry is an equivalent trigger",
|
|
"your first action MUST be to run `tutti-dev connector available --json`",
|
|
"Currently enabled by the user: none",
|
|
"CLI and MCP pick authority by whose data the task touches",
|
|
"connectorAuthority",
|
|
"TUTTI_CONNECTOR_CLI_REQUESTED_AUTHORITY=caller",
|
|
"Skills execute as the Owner",
|
|
"ask the user which side to use before calling",
|
|
"Each call commits to one authority",
|
|
"not re-sent as the other side",
|
|
"When a connector MCP error is `-33001` or `-33002`",
|
|
"mention://connector-authorization/<connectorKey>?authority=<authority>&subject=<subject>",
|
|
"If `subject` is missing, do not write a mention",
|
|
} {
|
|
if !strings.Contains(shared, want) {
|
|
t.Fatalf("shared policy missing %q: %s", want, shared)
|
|
}
|
|
}
|
|
if strings.Contains(shared, "CLI defaults to Owner") ||
|
|
strings.Contains(shared, "Never use a same-name") ||
|
|
strings.Contains(shared, "Skills and MCP execute as the Owner") ||
|
|
strings.Contains(shared, "CLI executes as either side") ||
|
|
strings.Contains(shared, "recoveryMention") ||
|
|
strings.Contains(shared, "Copy the href unchanged") {
|
|
t.Fatalf("shared policy leaked legacy wording: %s", shared)
|
|
}
|
|
}
|
|
|
|
func TestEnabledConnectorsIndexIsDeterministicUniqueOrNone(t *testing.T) {
|
|
if got := enabledConnectorsIndex(nil); got != "none" {
|
|
t.Fatalf("nil keys = %q, want none", got)
|
|
}
|
|
if got := enabledConnectorsIndex([]string{" ", ""}); got != "none" {
|
|
t.Fatalf("blank keys = %q, want none", got)
|
|
}
|
|
if got := enabledConnectorsIndex([]string{"lark-cli", "github", "lark-cli", " github "}); got != "github, lark-cli" {
|
|
t.Fatalf("unique keys = %q, want github, lark-cli", got)
|
|
}
|
|
}
|
|
|
|
func TestConnectorRoutingIndexIsDeterministicDeduplicatedAndBounded(t *testing.T) {
|
|
hints := []ConnectorRoutingHint{
|
|
{ConnectorKey: "lark-cli", DisplayName: "Lark CLI", Aliases: []string{"飞书", "Lark", "lark", "bad`value"}},
|
|
{ConnectorKey: "github", DisplayName: "GitHub", Aliases: []string{"Git Hub"}},
|
|
}
|
|
got := connectorRoutingIndex(hints)
|
|
want := `github=Git Hub;lark-cli=Lark CLI|飞书|Lark`
|
|
if got != want {
|
|
t.Fatalf("connectorRoutingIndex() = %s, want %s", got, want)
|
|
}
|
|
if exported := ConnectorRoutingIndex(hints); exported != want {
|
|
t.Fatalf("ConnectorRoutingIndex() = %s, want internal rendering %s", exported, want)
|
|
}
|
|
|
|
large := make([]ConnectorRoutingHint, 0, 40)
|
|
for index := 0; index < 40; index++ {
|
|
large = append(large, ConnectorRoutingHint{ConnectorKey: fmt.Sprintf("connector-%02d", index),
|
|
DisplayName: strings.Repeat("a", 48), Aliases: []string{strings.Repeat("b", 48), strings.Repeat("c", 48)}})
|
|
}
|
|
if count := utf8.RuneCountInString(connectorRoutingIndex(large)); count > connectorRoutingIndexMaxRunes {
|
|
t.Fatalf("connector routing index chars = %d, want <= %d", count, connectorRoutingIndexMaxRunes)
|
|
}
|
|
}
|
|
|
|
func TestProviderSkillRootDoesNotExposeClaudeCodeProjectSkills(t *testing.T) {
|
|
cwd := filepath.Join("workspace", "repo")
|
|
if root := providerSkillRoot(cwd, "claude-code"); root != "" {
|
|
t.Fatalf("providerSkillRoot() for claude-code = %q", root)
|
|
}
|
|
if root := providerSkillRoot(cwd, "hermes"); root != filepath.Join(cwd, ".agent_context", "skills") {
|
|
t.Fatalf("providerSkillRoot() for hermes = %q", root)
|
|
}
|
|
if root := providerSkillRoot(cwd, "open-claw"); root != filepath.Join(cwd, ".openclaw", "skills") {
|
|
t.Fatalf("providerSkillRoot() for open-claw = %q", root)
|
|
}
|
|
}
|
|
|
|
func TestRenderSkillBundleIncludesGuideAndOptionalSkills(t *testing.T) {
|
|
t.Setenv(browserUseSwitchEnv, "")
|
|
t.Setenv(computerUseSwitchEnv, "")
|
|
preparer := newTestPreparer(t.TempDir())
|
|
preparer.CLICommand = "tutti-dev"
|
|
preparer.ComputerUseAvailable = func() bool { return true }
|
|
|
|
bundle, err := preparer.RenderSkillBundle(t.Context(), PrepareInput{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-1",
|
|
AgentTargetID: "local:codex",
|
|
Provider: "codex",
|
|
BrowserUse: true,
|
|
ComputerUse: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bundle.SchemaVersion != 2 || bundle.CLICommand != "tutti-dev" {
|
|
t.Fatalf("bundle metadata = %#v", bundle)
|
|
}
|
|
wantSlugs := "tutti-cli,tutti-handoff,tutti-model-allocation,issue-manager,workspace-app,reference,browser-use,computer-use"
|
|
if got := strings.Join(skillBundleSlugs(bundle.Skills), ","); got != wantSlugs {
|
|
t.Fatalf("skill slugs = %q", got)
|
|
}
|
|
tuttiSkill := skillBundleRecord(bundle.Skills, tuttiSkillName)
|
|
for _, expected := range []string{
|
|
"Never inspect or modify `~/.tutti*/*.db`",
|
|
"tutti-dev plan issue get --issue-id <issue-id> --json",
|
|
"tutti-dev plan issue resume --issue-id <issue-id> --json",
|
|
"If the snapshot reports `dispatchPaused: true`, do not retry schedule",
|
|
"`task_failed` or `task_canceled`",
|
|
"Rework it with a new `taskId`",
|
|
"Recovery is bounded",
|
|
"On `inactive_checkpoint` or `stale_graph_revision`",
|
|
} {
|
|
if !strings.Contains(tuttiSkill.Content, expected) {
|
|
t.Fatalf("tutti skill missing recovery rule %q: %q", expected, tuttiSkill.Content)
|
|
}
|
|
}
|
|
handoffSkill := skillBundleRecord(bundle.Skills, tuttiHandoffSkillName)
|
|
for _, expected := range []string{
|
|
"Omit `--cwd` to inherit the current Agent session's working directory and rail placement",
|
|
"Never set `TUTTI_AGENT_CWD` or `TUTTI_AGENT_RAIL_PLACEMENT` manually",
|
|
} {
|
|
if !strings.Contains(handoffSkill.Content, expected) {
|
|
t.Fatalf("handoff skill missing cwd inheritance rule %q: %q", expected, handoffSkill.Content)
|
|
}
|
|
}
|
|
if bundle.RecommendedSystemPrompt == nil {
|
|
t.Fatal("missing recommended system prompt")
|
|
}
|
|
for _, expected := range []string{
|
|
"Generic subagents use provider-native tools; `$tutti-handoff` is for explicit Tutti handoffs or `mention://agent-target/...`.",
|
|
} {
|
|
if !strings.Contains(bundle.RecommendedSystemPrompt.Content, expected) {
|
|
t.Fatalf("recommended system prompt missing routing boundary %q: %q", expected, bundle.RecommendedSystemPrompt.Content)
|
|
}
|
|
}
|
|
guide, ok := skillBundleFileContent(tuttiSkill, commandGuideReferencePath)
|
|
if !ok || !strings.Contains(guide, "tutti-dev issue get --issue-id <issue-id> --json") {
|
|
t.Fatalf("command guide = %q", guide)
|
|
}
|
|
modelAllocation := skillBundleRecord(bundle.Skills, tuttiModelAllocationSkillName)
|
|
if !strings.Contains(modelAllocation.Content, "The required tier is `max(task tier, effect floor)`") {
|
|
t.Fatalf("model allocation skill = %q", modelAllocation.Content)
|
|
}
|
|
if !strings.Contains(modelAllocation.Content, "parallel target") ||
|
|
!strings.Contains(modelAllocation.Content, "| 75-100 | 4") {
|
|
t.Fatalf("model allocation parallel policy = %q", modelAllocation.Content)
|
|
}
|
|
modelTiers, ok := skillBundleFileContent(modelAllocation, tuttiModelAllocationReferencePath)
|
|
for _, expected := range []string{
|
|
"`gpt-5.6-luna`",
|
|
"`gpt-5.6-terra`",
|
|
"`gpt-5.6-sol`",
|
|
"`gpt-5.6-sol-pro`",
|
|
"`composer-2.5`",
|
|
"`grok-4.5`",
|
|
"`kimi-k3`",
|
|
"`moonshotai/kimi-k3`",
|
|
"`anthropic/claude-opus-4.8`",
|
|
} {
|
|
if !ok || !strings.Contains(modelTiers, expected) {
|
|
t.Fatalf("model tier reference missing %q: %q", expected, modelTiers)
|
|
}
|
|
}
|
|
for _, expected := range []string{
|
|
"Compare joint `(agentTargetId, model, reasoningEffort, permissionModeId)`",
|
|
"receive no suitability bonus",
|
|
"prefer a non-planning target",
|
|
} {
|
|
if !strings.Contains(modelAllocation.Content, expected) {
|
|
t.Fatalf("model allocation skill missing anti-affinity rule %q: %q", expected, modelAllocation.Content)
|
|
}
|
|
}
|
|
browser := skillBundleRecord(bundle.Skills, browserUseSkillName).Content
|
|
computer := skillBundleRecord(bundle.Skills, computerUseSkillName).Content
|
|
if !strings.Contains(browser, "tutti-dev browser open --url <url> --json") {
|
|
t.Fatalf("browser skill missing rendered command: %s", browser)
|
|
}
|
|
for _, want := range []string{
|
|
"tutti-dev computer screenshot --json",
|
|
"tutti-dev computer tool describe --name <tool> --json",
|
|
"--arguments-json -",
|
|
"element_token",
|
|
} {
|
|
if !strings.Contains(computer, want) {
|
|
t.Fatalf("computer skill missing %q: %s", want, computer)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderSkillBundleOmitsUnavailableComputerUse(t *testing.T) {
|
|
t.Setenv(browserUseSwitchEnv, "")
|
|
t.Setenv(computerUseSwitchEnv, "")
|
|
preparer := newTestPreparer(t.TempDir())
|
|
preparer.ComputerUseAvailable = func() bool { return false }
|
|
|
|
bundle, err := preparer.RenderSkillBundle(t.Context(), PrepareInput{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-1",
|
|
AgentTargetID: "local:codex",
|
|
Provider: "codex",
|
|
ComputerUse: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(strings.Join(skillBundleSlugs(bundle.Skills), ","), "computer-use") {
|
|
t.Fatalf("computer-use should be unavailable: %#v", bundle.Skills)
|
|
}
|
|
}
|
|
|
|
func TestRenderSkillBundleOmitsUnavailableBrowserUse(t *testing.T) {
|
|
t.Setenv(browserUseSwitchEnv, "")
|
|
preparer := newTestPreparer(t.TempDir())
|
|
preparer.BrowserUseAvailable = func() bool { return false }
|
|
|
|
bundle, err := preparer.RenderSkillBundle(t.Context(), PrepareInput{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-1",
|
|
AgentTargetID: "local:codex",
|
|
Provider: "codex",
|
|
BrowserUse: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(strings.Join(skillBundleSlugs(bundle.Skills), ","), "browser-use") {
|
|
t.Fatalf("browser-use should be unavailable: %#v", bundle.Skills)
|
|
}
|
|
}
|
|
|
|
func TestRenderProviderSkillBundleIncludesClaudeRouting(t *testing.T) {
|
|
input := testInputWithCommands(t, PrepareInput{
|
|
AgentSessionID: "session-1",
|
|
AgentTargetID: "local:claude",
|
|
CLICommand: "tutti-dev",
|
|
Provider: "claude",
|
|
})
|
|
resolved, err := resolveCapabilities(t.Context(), input, StandardProfile(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input.resolved = resolved
|
|
bundle, err := renderProviderSkillBundle(input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bundle.RecommendedSystemPrompt == nil {
|
|
t.Fatal("missing recommended system prompt")
|
|
}
|
|
for _, want := range []string{
|
|
"Claude Code mention routing",
|
|
`Skill(skill="tutti-cli:workspace-app")`,
|
|
`Skill(skill="tutti-cli:tutti-handoff")`,
|
|
"Do not use `ToolSearch`",
|
|
} {
|
|
if !strings.Contains(bundle.RecommendedSystemPrompt.Content, want) {
|
|
t.Fatalf("recommended prompt missing %q: %s", want, bundle.RecommendedSystemPrompt.Content)
|
|
}
|
|
}
|
|
}
|
|
|
|
func skillBundleSlugs(skills []SkillMaterializationRecord) []string {
|
|
slugs := make([]string, 0, len(skills))
|
|
for _, skill := range skills {
|
|
slugs = append(slugs, skill.Slug)
|
|
}
|
|
return slugs
|
|
}
|
|
|
|
func skillBundleRecord(skills []SkillMaterializationRecord, slug string) SkillMaterializationRecord {
|
|
for _, skill := range skills {
|
|
if skill.Slug == slug {
|
|
return skill
|
|
}
|
|
}
|
|
return SkillMaterializationRecord{}
|
|
}
|
|
|
|
func skillBundleFileContent(skill SkillMaterializationRecord, path string) (string, bool) {
|
|
for _, file := range skill.Files {
|
|
if file.Path == path {
|
|
return file.Content, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|