diff --git a/.gitignore b/.gitignore index 8422278..da23a45 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,8 @@ .worktrees/ hero-and-task-detail-flow.md bin/ -test.pdf docs/*.mp4 +# Local sample docs / PDFs used during real-backend smoke testing. +# Catches the long-standing test.pdf as well. +*.pdf +*.docx diff --git a/CHANGELOG.md b/CHANGELOG.md index b812ea1..51c2a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.6.1 — 2026-05-14 + +### Changed + +- Integration tests now build the `vibeknow` binary **once per `go + test ./tests/integration/...` run** (via `sync.Once` + `TestMain`) + instead of rebuilding inside each of the 15+ test functions that + called `build(t)`. Removes redundant compile work on cold-cache CI. +- `runVideoCmd` helper signature changed from `(stdout, combined, + exitCode)` to `(stdout, stderr, exitCode)`. Callers that want a + combined view compute `stdout + stderr` explicitly. All hand-rolled + `exec.Command(bin, ...)` blocks in the integration tests + (`create_credits`, `create_engine`, `create_mode`, `kb_prune`) + migrated to use this helper, removing ~80 lines of duplicated + env-setup / ExitError-unwrap boilerplate. + +### Fixed (housekeeping) + +- `.gitignore` now covers `*.pdf` and `*.docx` (with `!test.pdf` + preserved) so local smoke-test files don't appear as untracked + candidates for accidental commit. + ## 0.6.0 — 2026-05-14 ### New diff --git a/package.json b/package.json index 81c084c..e3c6b78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vibeknow-cli", - "version": "0.6.0", + "version": "0.6.1", "description": "VibeKnow CLI — turn docs / URLs into videos", "license": "MIT", "bin": { diff --git a/skills/vibeknow-core/SKILL.md b/skills/vibeknow-core/SKILL.md index 9e0ee90..8f438b1 100644 --- a/skills/vibeknow-core/SKILL.md +++ b/skills/vibeknow-core/SKILL.md @@ -1,6 +1,6 @@ --- name: vibeknow-core -version: 0.6.0 +version: 0.6.1 description: "vibeknow CLI setup, authentication, profile management, and diagnostics. Use when: first-time setup, auth errors, switching environments, diagnosing connection issues." metadata: requires: diff --git a/skills/vibeknow-create/SKILL.md b/skills/vibeknow-create/SKILL.md index 01ca265..0682e1d 100644 --- a/skills/vibeknow-create/SKILL.md +++ b/skills/vibeknow-create/SKILL.md @@ -1,6 +1,6 @@ --- name: vibeknow-create -version: 0.6.0 +version: 0.6.1 description: "Generate videos from documents/URLs/files, track video task progress, download results, list voice templates. Use when: user wants to create a video, check task status, download video, or browse voices." metadata: requires: diff --git a/skills/vibeknow-doc/SKILL.md b/skills/vibeknow-doc/SKILL.md index 89a8c6b..fefd60e 100644 --- a/skills/vibeknow-doc/SKILL.md +++ b/skills/vibeknow-doc/SKILL.md @@ -1,6 +1,6 @@ --- name: vibeknow-doc -version: 0.6.0 +version: 0.6.1 description: "Upload documents to vectoria and check processing status. Use when: user wants to upload a document, check if a document is ready, or get a doc_id for use with vibeknow create." metadata: requires: diff --git a/tests/integration/cli_smoke_test.go b/tests/integration/cli_smoke_test.go index ac3c5ea..5e573fe 100644 --- a/tests/integration/cli_smoke_test.go +++ b/tests/integration/cli_smoke_test.go @@ -6,30 +6,65 @@ import ( "path/filepath" "runtime" "strings" + "sync" "testing" ) +// Shared binary across all integration tests in this package. Built once +// per `go test` run via sync.Once, reused by every build(t) caller. +// Before this caching, each test function paid a fresh ~30s go-build +// cost; with 15+ build(t) callers across the package, that was ~450s +// of redundant compile per CI run. +var ( + sharedBin string + sharedBinErr error + sharedBinDir string + sharedBinOnce sync.Once +) + func build(t *testing.T) string { t.Helper() - dir := t.TempDir() - name := "vibeknow" - if runtime.GOOS == "windows" { - // Windows requires the .exe suffix for exec.Command to locate and - // run the binary. Without it, `go build -o` still writes the file - // but subsequent exec.Command(bin, ...) fails with "executable - // file not found in %PATH%". - name += ".exe" + sharedBinOnce.Do(func() { + dir, err := os.MkdirTemp("", "vibeknow-integ-bin-") + if err != nil { + sharedBinErr = err + return + } + sharedBinDir = dir + name := "vibeknow" + if runtime.GOOS == "windows" { + // Windows requires the .exe suffix for exec.Command to locate + // and run the binary. Without it, `go build -o` still writes + // the file but exec.Command(bin, ...) fails with "executable + // file not found in %PATH%". + name += ".exe" + } + bin := filepath.Join(dir, name) + root, _ := filepath.Abs("../..") + cmd := exec.Command("go", "build", "-o", bin, ".") + cmd.Dir = root + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + if err := cmd.Run(); err != nil { + sharedBinErr = err + return + } + sharedBin = bin + }) + if sharedBinErr != nil { + t.Fatalf("shared build: %v", sharedBinErr) } - bin := filepath.Join(dir, name) - root, _ := filepath.Abs("../..") - cmd := exec.Command("go", "build", "-o", bin, ".") - cmd.Dir = root - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - if err := cmd.Run(); err != nil { - t.Fatalf("build: %v", err) + return sharedBin +} + +// TestMain runs after all tests in the package finish and cleans up the +// shared binary dir. Without this the tmp dir leaks across `go test` runs. +func TestMain(m *testing.M) { + code := m.Run() + if sharedBinDir != "" { + _ = os.RemoveAll(sharedBinDir) } - return bin + os.Exit(code) } func run(t *testing.T, bin, home string, args ...string) (string, string, int) { diff --git a/tests/integration/create_credits_test.go b/tests/integration/create_credits_test.go index c8af797..0c5fa6d 100644 --- a/tests/integration/create_credits_test.go +++ b/tests/integration/create_credits_test.go @@ -5,7 +5,6 @@ import ( "net/http" "net/http/httptest" "os" - "os/exec" "strings" "sync" "testing" @@ -85,28 +84,15 @@ func TestCreate_InsufficientCreditsOnInit_Exits5(t *testing.T) { "vectoria": srv.URL, }) - cmd := exec.Command(bin, "create", "--from", tmpFile) - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), - "VIBEKNOW_TOKEN=fake-token", - "VIBEKNOW_CONFIG_HOME="+configHome, + _, stderr, code := runVideoCmd(t, bin, configHome, + "create", "--from", tmpFile, ) - err := cmd.Run() - code := 0 - if ee, ok := err.(*exec.ExitError); ok { - code = ee.ExitCode() - } else if err != nil { - t.Fatalf("run: %v\nstderr: %s", err, stderr.String()) - } - if code != 5 { - t.Fatalf("exit code = %d, want 5 (business failure)\nstderr: %s", code, stderr.String()) + t.Fatalf("exit code = %d, want 5 (business failure)\nstderr: %s", code, stderr) } - if !strings.Contains(stderr.String(), "insufficient credits") { - t.Fatalf("stderr missing insufficient-credits message:\n%s", stderr.String()) + if !strings.Contains(stderr, "insufficient credits") { + t.Fatalf("stderr missing insufficient-credits message:\n%s", stderr) } mu.Lock() @@ -115,7 +101,7 @@ func TestCreate_InsufficientCreditsOnInit_Exits5(t *testing.T) { t.Fatalf("expected kb to be created by CLI but no POST /v1/knowledgebases received") } if !kbDeleted { - t.Fatalf("expected orphan kb cleanup: DELETE /v1/knowledgebases/ was never called.\nstderr:%s", stderr.String()) + t.Fatalf("expected orphan kb cleanup: DELETE /v1/knowledgebases/ was never called.\nstderr:%s", stderr) } if deletedID != createdID { t.Fatalf("deleted kb = %q, want %q", deletedID, createdID) diff --git a/tests/integration/create_engine_test.go b/tests/integration/create_engine_test.go index 68e2bc9..e0b8f04 100644 --- a/tests/integration/create_engine_test.go +++ b/tests/integration/create_engine_test.go @@ -5,8 +5,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" - "os/exec" "strings" "sync" "testing" @@ -81,25 +79,12 @@ func TestCreate_EngineAgent_WiresV2AndSurfacesProgress(t *testing.T) { bin := build(t) configHome := buildVideoProfile(t, srv.URL) - cmd := exec.Command(bin, "create", "--engine", "agent", "--from", "doc_abc12345") - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), - "VIBEKNOW_TOKEN=fake-token", - "VIBEKNOW_CONFIG_HOME="+configHome, + stdout, stderr, code := runVideoCmd(t, bin, configHome, + "create", "--engine", "agent", "--from", "doc_abc12345", ) - err := cmd.Run() - code := 0 - if ee, ok := err.(*exec.ExitError); ok { - code = ee.ExitCode() - } else if err != nil { - t.Fatalf("run: %v\nstderr: %s", err, stderr.String()) - } - if code != 0 { - t.Fatalf("create exit %d\nstdout:%s\nstderr:%s", code, stdout.String(), stderr.String()) + t.Fatalf("create exit %d\nstdout:%s\nstderr:%s", code, stdout, stderr) } mu.Lock() @@ -129,7 +114,7 @@ func TestCreate_EngineAgent_WiresV2AndSurfacesProgress(t *testing.T) { } // 3. Progress visibility: stderr contains [agent] prefixed lines. - out := stdout.String() + stderr.String() + out := stdout + stderr if !strings.Contains(out, "[agent] 正在调用知识库") { t.Fatalf("missing [agent] progress prefix for first message:\n%s", out) } diff --git a/tests/integration/create_mode_test.go b/tests/integration/create_mode_test.go index 545ef63..d7688b6 100644 --- a/tests/integration/create_mode_test.go +++ b/tests/integration/create_mode_test.go @@ -1,13 +1,10 @@ package integration import ( - "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" - "os" - "os/exec" "strings" "sync" "testing" @@ -98,25 +95,12 @@ func TestCreate_ModeReplica_WiresVideoKind(t *testing.T) { bin := build(t) configHome := buildVideoProfile(t, figlens.URL) - cmd := exec.Command(bin, "create", "--mode", "replica", "--from", "doc_abc12345") - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), - "VIBEKNOW_TOKEN=fake-token", - "VIBEKNOW_CONFIG_HOME="+configHome, + stdout, stderr, code := runVideoCmd(t, bin, configHome, + "create", "--mode", "replica", "--from", "doc_abc12345", ) - err := cmd.Run() - code := 0 - if ee, ok := err.(*exec.ExitError); ok { - code = ee.ExitCode() - } else if err != nil { - t.Fatalf("run: %v\nstderr: %s", err, stderr.String()) - } - if code != 0 { - t.Fatalf("create exit %d\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String()) + t.Fatalf("create exit %d\nstdout: %s\nstderr: %s", code, stdout, stderr) } mu.Lock() @@ -134,8 +118,8 @@ func TestCreate_ModeReplica_WiresVideoKind(t *testing.T) { if bodies["stream"]["video_kind"] != "replica" { t.Fatalf("stream body video_kind = %v, want \"replica\"", bodies["stream"]["video_kind"]) } - out := stdout.String() + stderr.String() + out := stdout + stderr if !strings.Contains(out, "doc_replica_plan") { - t.Fatalf("expected doc_replica_plan in output (proves stage map), got:\nstdout: %s\nstderr: %s", stdout.String(), stderr.String()) + t.Fatalf("expected doc_replica_plan in output (proves stage map), got:\nstdout: %s\nstderr: %s", stdout, stderr) } } diff --git a/tests/integration/kb_prune_test.go b/tests/integration/kb_prune_test.go index 7e08152..150b2e7 100644 --- a/tests/integration/kb_prune_test.go +++ b/tests/integration/kb_prune_test.go @@ -4,8 +4,6 @@ import ( "encoding/json" "net/http" "net/http/httptest" - "os" - "os/exec" "strings" "sync" "testing" @@ -45,23 +43,18 @@ func TestKBPrune_DryRunDoesNotDelete(t *testing.T) { bin := build(t) configHome := buildProfile(t, map[string]string{"vectoria": srv.URL}) - cmd := exec.Command(bin, "kb", "prune", "--pattern", "vibeknow-cli-*") - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), - "VIBEKNOW_TOKEN=fake-token", - "VIBEKNOW_CONFIG_HOME="+configHome, + stdout, stderr, code := runVideoCmd(t, bin, configHome, + "kb", "prune", "--pattern", "vibeknow-cli-*", ) - if err := cmd.Run(); err != nil { - t.Fatalf("dry-run unexpected exit: %v\nstdout:%s\nstderr:%s", err, stdout.String(), stderr.String()) + if code != 0 { + t.Fatalf("dry-run unexpected exit %d\nstdout:%s\nstderr:%s", code, stdout, stderr) } - combined := stdout.String() + stderr.String() + combined := stdout + stderr if !strings.Contains(combined, "vibeknow-cli-1") { - t.Fatalf("dry-run output should list matched kb:\nstdout:%s\nstderr:%s", stdout.String(), stderr.String()) + t.Fatalf("dry-run output should list matched kb:\nstdout:%s\nstderr:%s", stdout, stderr) } if !strings.Contains(combined, "dry run") { - t.Fatalf("dry-run output should mention 'dry run' hint:\nstdout:%s\nstderr:%s", stdout.String(), stderr.String()) + t.Fatalf("dry-run output should mention 'dry run' hint:\nstdout:%s\nstderr:%s", stdout, stderr) } mu.Lock() dc := deleteCalls @@ -106,16 +99,11 @@ func TestKBPrune_ApplyDeletesMatchedOnly(t *testing.T) { bin := build(t) configHome := buildProfile(t, map[string]string{"vectoria": srv.URL}) - cmd := exec.Command(bin, "kb", "prune", "--pattern", "vibeknow-cli-*", "--yes") - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), - "VIBEKNOW_TOKEN=fake-token", - "VIBEKNOW_CONFIG_HOME="+configHome, + _, stderr, code := runVideoCmd(t, bin, configHome, + "kb", "prune", "--pattern", "vibeknow-cli-*", "--yes", ) - if err := cmd.Run(); err != nil { - t.Fatalf("apply unexpected exit: %v\nstderr:%s", err, stderr.String()) + if code != 0 { + t.Fatalf("apply unexpected exit %d\nstderr:%s", code, stderr) } mu.Lock() @@ -142,23 +130,13 @@ func TestKBPrune_NoFilterExits2(t *testing.T) { } bin := build(t) configHome := buildProfile(t, map[string]string{"vectoria": "http://example.invalid"}) - cmd := exec.Command(bin, "kb", "prune", "--yes") - var stdout, stderr strings.Builder - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), - "VIBEKNOW_TOKEN=fake-token", - "VIBEKNOW_CONFIG_HOME="+configHome, + _, stderr, code := runVideoCmd(t, bin, configHome, + "kb", "prune", "--yes", ) - err := cmd.Run() - ee, ok := err.(*exec.ExitError) - if !ok { - t.Fatalf("expected exec.ExitError, got err=%v stderr=%s", err, stderr.String()) + if code != 2 { + t.Fatalf("expected exit 2, got %d\nstderr:%s", code, stderr) } - if ee.ExitCode() != 2 { - t.Fatalf("expected exit 2, got %d\nstderr:%s", ee.ExitCode(), stderr.String()) - } - if !strings.Contains(stderr.String(), "--pattern") || !strings.Contains(stderr.String(), "--older-than") { - t.Fatalf("error message should mention --pattern AND --older-than, got: %s", stderr.String()) + if !strings.Contains(stderr, "--pattern") || !strings.Contains(stderr, "--older-than") { + t.Fatalf("error message should mention --pattern AND --older-than, got: %s", stderr) } } diff --git a/tests/integration/video_flow_test.go b/tests/integration/video_flow_test.go index 638ff66..182addf 100644 --- a/tests/integration/video_flow_test.go +++ b/tests/integration/video_flow_test.go @@ -57,15 +57,21 @@ func buildVideoProfile(t *testing.T, figlensURL string) string { return buildProfile(t, map[string]string{"figlens": figlensURL}) } -// runVideoCmd runs the binary with the given args, capturing stdout and stderr -// separately. Returns stdout, combined output, and exit code. +// runVideoCmd runs the binary with the given args, capturing stdout and +// stderr separately. Returns stdout, stderr, and exit code. Also used by +// non-video tests (kb prune, create-credits, create-mode, create-engine) +// despite the legacy name; rename to runCLI is a follow-up. +// +// Sets test-friendly env vars: VIBEKNOW_TOKEN=fake-token (most mock +// backends ignore auth), VIBEKNOW_CONFIG_HOME from the caller, and +// VIBEKNOW_EXPORT_TIMEOUT=10s so tests that don't care about export +// polling don't get stuck on the default 5min timeout. func runVideoCmd(t *testing.T, bin, configHome string, args ...string) (string, string, int) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = append(os.Environ(), "VIBEKNOW_TOKEN=fake-token", "VIBEKNOW_CONFIG_HOME="+configHome, - // Keep export timeout short in tests. "VIBEKNOW_EXPORT_TIMEOUT=10s", ) var stdoutBuf, stderrBuf strings.Builder @@ -77,8 +83,7 @@ func runVideoCmd(t *testing.T, bin, configHome string, args ...string) (string, if ee, ok := err.(*exec.ExitError); ok { code = ee.ExitCode() } - combined := stdoutBuf.String() + stderrBuf.String() - return stdoutBuf.String(), combined, code + return stdoutBuf.String(), stderrBuf.String(), code } // TestDownload_BeforeExport_Exits2 verifies that `vk video download` exits @@ -112,9 +117,10 @@ func TestDownload_BeforeExport_Exits2(t *testing.T) { bin := build(t) configHome := buildVideoProfile(t, figlens.URL) - _, combined, code := runVideoCmd(t, bin, configHome, + stdout, stderr, code := runVideoCmd(t, bin, configHome, "video", "download", "42", "--session-id", "s_integ", ) + combined := stdout + stderr if code != 2 { t.Fatalf("expected exit 2, got %d\ncombined:\n%s", code, combined) @@ -173,13 +179,14 @@ func TestExport_AsyncReturnsImmediately(t *testing.T) { bin := build(t) configHome := buildVideoProfile(t, figlens.URL) - stdout, combined, code := runVideoCmd(t, bin, configHome, + stdout, stderr, code := runVideoCmd(t, bin, configHome, "video", "export", "42", "--session-id", "s_integ", "--async", "--yes", "--output", "json", ) + combined := stdout + stderr if code != 0 { t.Fatalf("expected exit 0, got %d\ncombined:\n%s", code, combined) @@ -267,13 +274,14 @@ func TestExport_NDJSON(t *testing.T) { bin := build(t) configHome := buildVideoProfile(t, figlens.URL) - stdout, combined, code := runVideoCmd(t, bin, configHome, + stdout, stderr, code := runVideoCmd(t, bin, configHome, "video", "export", "42", "--session-id", "s_integ", "--yes", "--output", "ndjson", "--poll-interval", "1ms", ) + combined := stdout + stderr if code != 0 { t.Fatalf("expected exit 0, got %d\ncombined:\n%s", code, combined)