Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 97 additions & 3 deletions labs/22-yield/yield/cmd/yskill/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"flag"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/debug"
Expand Down Expand Up @@ -210,8 +211,10 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
t.Fatalf("skill.json = version %d language %q", generated.Version, generated.Language)
}
skill := readTestFile(t, filepath.Join(dir, "SKILL.md"))
if !strings.Contains(skill, tt.command) {
t.Fatalf("SKILL.md does not contain %q:\n%s", tt.command, skill)
workflow := shellQuoteForPlatform(dir, runtime.GOOS)
command := strings.Replace(tt.command, " .", " "+workflow, 1)
if !strings.Contains(skill, command) {
t.Fatalf("SKILL.md does not contain %q:\n%s", command, skill)
}
entrypoint := map[string]string{
"typescript": "main.ts",
Expand Down Expand Up @@ -244,6 +247,97 @@ func TestScaffoldSkillWritesLanguageSpecificEntrypoints(t *testing.T) {
}
}

func TestPackageScaffoldsPrintCreatedWorkflowInNextCommands(t *testing.T) {
previousVersion := version
version = "0.1.28"
t.Cleanup(func() { version = previousVersion })

for _, tt := range []struct {
language string
launcher string
}{
{language: "typescript", launcher: "npm exec -- yskill"},
{language: "python", launcher: "python -m yieldskill"},
} {
t.Run(tt.language, func(t *testing.T) {
root := filepath.Join(t.TempDir(), "project with space")
dir := filepath.Join(root, "skills", "safe-change")
output := captureStdout(t, func() {
if err := scaffoldSkill(dir, tt.language, "", "Check a safe change before applying it."); err != nil {
t.Fatal(err)
}
})
workflow := shellQuoteForPlatform(dir, runtime.GOOS)
for _, line := range []string{
"test: " + tt.launcher + " doctor " + workflow + " --test",
"then: " + tt.launcher + " register " + workflow,
} {
if !strings.Contains(output, line) {
t.Fatalf("init output does not contain %q:\n%s", line, output)
}
}
})
}
}

func TestShellQuoteForPlatform(t *testing.T) {
for _, tt := range []struct {
name, value, goos, want string
}{
{name: "relative", value: "skills/review", goos: "linux", want: "'skills/review'"},
{name: "explicit relative", value: "./skills/review", goos: "linux", want: "'./skills/review'"},
{name: "absolute", value: "/tmp/project/skills/review", goos: "linux", want: "'/tmp/project/skills/review'"},
{name: "nested with space", value: "workflows/team one/safe change's", goos: "linux", want: `'workflows/team one/safe change'"'"'s'`},
{name: "windows", value: `skills\safe change's`, goos: "windows", want: `'skills\safe change''s'`},
} {
t.Run(tt.name, func(t *testing.T) {
if got := shellQuoteForPlatform(tt.value, tt.goos); got != tt.want {
t.Fatalf("quote = %q, want %q", got, tt.want)
}
})
}
}

func TestRustScaffoldNamesPrimaryBinary(t *testing.T) {
previousVersion := version
version = "0.1.28"
t.Cleanup(func() { version = previousVersion })
dir := filepath.Join(t.TempDir(), "safe-change")
if err := scaffoldSkill(dir, "rust", "", "Check a safe change before applying it."); err != nil {
t.Fatal(err)
}
manifest := readTestFile(t, filepath.Join(dir, "skill.json"))
if manifest != "{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",\"safe-change\"]}\n" {
t.Fatalf("skill.json = %q", manifest)
}
}

func TestRustScaffoldRunsPrimaryBinaryWhenFixtureAddsAnotherBinary(t *testing.T) {
if _, err := exec.LookPath("cargo"); err != nil {
t.Skip("cargo is not installed")
}
dir := filepath.Join(t.TempDir(), "safe-change")
if err := scaffoldSkill(dir, "rust", "", "Check a safe change before applying it."); err != nil {
t.Fatal(err)
}
writeTestFile(t, filepath.Join(dir, "Cargo.toml"), "[package]\nname = \"safe-change\"\nversion = \"0.1.0\"\nedition = \"2021\"\n")
writeTestFile(t, filepath.Join(dir, "src", "main.rs"), "fn main() { println!(\"workflow\"); }\n")
writeTestFile(t, filepath.Join(dir, "src", "bin", "fixture-helper.rs"), "fn main() { println!(\"fixture\"); }\n")
manifest, err := readSkillManifest(dir)
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(manifest.Run[0], manifest.Run[1:]...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("generated run command failed: %v\n%s", err, out)
}
if strings.TrimSpace(string(out)) != "workflow" {
t.Fatalf("generated run command selected %q", out)
}
}

func TestGoScaffoldCanResolveItsPinnedModuleOnFirstRun(t *testing.T) {
previousVersion := version
previousTidyGoModule := tidyGoModule
Expand Down Expand Up @@ -285,7 +379,7 @@ func TestLocalGoAndRustScaffoldsKeepTheInvokedRuntime(t *testing.T) {
}
skill := readTestFile(t, filepath.Join(dir, "SKILL.md"))
launcher := repositoryRuntimeLauncher(filepath.Join(".yield", "bin", filepath.Base(localRuntimePath(repo))), runtime.GOOS)
workflow := shellQuote("skills/safe-change")
workflow := shellQuote(filepath.Join("skills", "safe-change"))
for _, command := range []string{
launcher + " run " + workflow,
launcher + " respond <run-id> --value <answer> --skill " + workflow,
Expand Down
13 changes: 10 additions & 3 deletions labs/22-yield/yield/cmd/yskill/scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func scaffoldCommand(language, dir string) (launcher, workflow string) {
"go": "yskill",
"rust": "yskill",
}[language]
workflow = "."
workflow = shellQuoteForPlatform(dir, runtime.GOOS)
if language != "go" && language != "rust" {
return launcher, workflow
}
Expand All @@ -65,7 +65,14 @@ func scaffoldCommand(language, dir string) (launcher, workflow string) {
if err != nil {
return launcher, workflow
}
return repositoryRuntimeLauncher(runtimeRel, runtime.GOOS), shellQuote(filepath.ToSlash(skillRel))
return repositoryRuntimeLauncher(runtimeRel, runtime.GOOS), shellQuoteForPlatform(skillRel, runtime.GOOS)
}

func shellQuoteForPlatform(value, goos string) string {
if goos == "windows" {
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
}
return shellQuote(filepath.ToSlash(value))
}

func scaffoldSkill(dir, language, sdkPath, description string) error {
Expand Down Expand Up @@ -155,7 +162,7 @@ func scaffoldFiles(name, language, sdkPath string) map[string]string {
".cargo/config.toml": "[registries.operatorstack]\nindex = \"sparse+https://get.operatorstack.systems/cargo/index/\"\n",
"Cargo.toml": fmt.Sprintf("[package]\nname = %q\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nyieldskill = { version = \"=%s\", registry = \"operatorstack\" }\nserde_json = \"1\"\n", name, v),
"src/main.rs": mainRust,
"skill.json": "{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\"]}\n",
"skill.json": fmt.Sprintf("{\"version\":1,\"language\":\"rust\",\"run\":[\"cargo\",\"run\",\"--quiet\",\"--bin\",%q]}\n", name),
}
default:
gomod := fmt.Sprintf("module %s\n\ngo 1.26.5\n\nrequire github.com/operatorstack/yield v%s\n", name, v)
Expand Down
4 changes: 4 additions & 0 deletions labs/22-yield/yield/docs/testing-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ first. `teardown` runs after success or failure. Every hook receives

Keep hooks small and repeatable. They should prepare or clean fixture state,
not replace the workflow behavior being tested.

For Rust, a fixture helper may add another binary under `src/bin/`. New
workflows name the primary workflow binary in `skill.json`, so Cargo still
runs the workflow without asking you to choose a binary.
4 changes: 2 additions & 2 deletions labs/22-yield/yield/evals/results/latest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema_version": 2,
"methodology_version": "1.1",
"generated_at": "2026-08-02T16:31:06.974Z",
"source_digest": "7e61b35562440679e70f58f61a73d06e13d6b823b72b36498a48a9156775dc20",
"generated_at": "2026-08-02T19:51:34.931Z",
"source_digest": "e5b21f13b239b762064fa3a4bd3d2354fba3dc46feb324f9bddbccad72827c24",
"status": "passed",
"workflow_conformance": {
"passed": 40,
Expand Down
6 changes: 6 additions & 0 deletions labs/22-yield/yield/release-notes/2026-08-02-final-dx-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Yield 0.1.28 final DX pass

- Print the created workflow path in TypeScript and Python `init` follow-up commands.
- Quote generated workflow paths for the active shell.
- Name the primary Rust binary in new workflow manifests so fixture helpers do not make `cargo run` ambiguous.
- Keep runtime and SDK behavior unchanged.
Loading