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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# Changelog

## 0.6.2 — 2026-05-14

### Fixed

- `vk create --from <vectoria-uuid>` now recognizes the UUID form
vectoria actually returns (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`,
lowercase hex) as a doc-id and skips the upload step. Previously
the `docIDRe` only accepted a hypothetical `doc_<alnum>` form that
vectoria has never used, so re-passing the `doc_id:` line printed
by a prior `vk create` run got treated as a local file path and
failed with "stat: no such file or directory". Backward compatible:
the legacy `doc_<alnum>` form still matches.

## 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
Expand Down
11 changes: 10 additions & 1 deletion cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ var (
flagCreateEngine string
)

var docIDRe = regexp.MustCompile(`^doc_[a-zA-Z0-9]{8,}$`)
// docIDRe matches a vectoria document identifier supplied via `--from`.
// Two forms are accepted:
// - Legacy CLI-coined form `doc_<8+ alnum>` (used by older callers).
// - Vectoria's native UUID form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
// (lowercase hex, what `vk create` itself prints on every run).
//
// Without the UUID branch, a user copying the `doc_id:` line from a prior
// successful run and re-passing it to `--from` would have the CLI treat
// it as a local file path, ending in "stat: no such file or directory".
var docIDRe = regexp.MustCompile(`^(doc_[a-zA-Z0-9]{8,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$`)

var createCmd = &cobra.Command{
Use: "create",
Expand Down
36 changes: 36 additions & 0 deletions cmd/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,39 @@ func TestEnginePipelineAllowsAllModes(t *testing.T) {
}
}

func TestDocIDRegex(t *testing.T) {
tests := []struct {
in string
match bool
desc string
}{
// Legacy form (kept for backward compat).
{"doc_abc12345", true, "legacy doc_<alnum>"},
{"doc_a1b2c3d4e5f6", true, "legacy long"},
{"doc_short", false, "legacy too-short suffix"},

// Vectoria native UUID form (what `vk create` actually prints).
{"3c498964-2c54-4ac0-97e8-1125d3bed640", true, "real vectoria UUID"},
{"00000000-0000-0000-0000-000000000000", true, "all-zero UUID"},

// Negative: file paths and URLs must not match.
{"./my-file.docx", false, "relative path"},
{"/abs/path.pdf", false, "absolute path"},
{"my-file.docx", false, "filename with extension"},
{"https://example.com/x", false, "URL"},

// Negative: malformed UUIDs.
{"3c498964-2c54-4ac0-97e8", false, "truncated UUID"},
{"3C498964-2C54-4AC0-97E8-1125D3BED640", false, "uppercase (vectoria emits lowercase)"},
{"3c498964_2c54_4ac0_97e8_1125d3bed640", false, "underscores not hyphens"},
{"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", false, "non-hex chars"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got := docIDRe.MatchString(tt.in)
if got != tt.match {
t.Fatalf("docIDRe.MatchString(%q) = %v, want %v", tt.in, got, tt.match)
}
})
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vibeknow-cli",
"version": "0.6.0",
"version": "0.6.2",
"description": "VibeKnow CLI — turn docs / URLs into videos",
"license": "MIT",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion skills/vibeknow-core/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: vibeknow-core
version: 0.6.0
version: 0.6.2
description: "vibeknow CLI setup, authentication, profile management, and diagnostics. Use when: first-time setup, auth errors, switching environments, diagnosing connection issues."
metadata:
requires:
Expand Down
2 changes: 1 addition & 1 deletion skills/vibeknow-create/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: vibeknow-create
version: 0.6.0
version: 0.6.2
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:
Expand Down
2 changes: 1 addition & 1 deletion skills/vibeknow-doc/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: vibeknow-doc
version: 0.6.0
version: 0.6.2
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:
Expand Down
69 changes: 52 additions & 17 deletions tests/integration/cli_smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
26 changes: 6 additions & 20 deletions tests/integration/create_credits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -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()
Expand All @@ -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/<id> was never called.\nstderr:%s", stderr.String())
t.Fatalf("expected orphan kb cleanup: DELETE /v1/knowledgebases/<id> was never called.\nstderr:%s", stderr)
}
if deletedID != createdID {
t.Fatalf("deleted kb = %q, want %q", deletedID, createdID)
Expand Down
23 changes: 4 additions & 19 deletions tests/integration/create_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down
26 changes: 5 additions & 21 deletions tests/integration/create_mode_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package integration

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
}
Loading
Loading