mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
add coverage
This commit is contained in:
parent
182a0fe29b
commit
682bcb8ace
7 changed files with 4002 additions and 0 deletions
|
|
@ -577,6 +577,21 @@ func TestLoadStatusesFromFile_EmptyStatuses(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadStatusRegistry_MalformedFile(t *testing.T) {
|
||||
cwdDir := setupLoadRegistryTest(t)
|
||||
|
||||
// write a workflow.yaml with invalid YAML so loadStatusRegistryFromFiles returns an error
|
||||
content := `statuses: [[[not valid yaml`
|
||||
if err := os.WriteFile(filepath.Join(cwdDir, "workflow.yaml"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := LoadStatusRegistry()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for malformed workflow.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadStatusRegistryFromFiles_AllFilesEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f1 := filepath.Join(dir, "workflow1.yaml")
|
||||
|
|
|
|||
|
|
@ -370,3 +370,53 @@ func TestLoadTriggerDefs_FileReadError(t *testing.T) {
|
|||
t.Fatal("expected error for unreadable workflow.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTriggerDefs_CwdEqualsProjectConfigDir(t *testing.T) {
|
||||
// when cwd == ProjectConfigDir(), candidates 2 and 3 resolve to the same
|
||||
// absolute path, exercising the seen[abs] dedup branch.
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
if err := os.MkdirAll(filepath.Join(userDir, "tiki"), 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
projectDir := t.TempDir()
|
||||
// resolve symlinks so projectRoot matches what filepath.Abs returns from cwd
|
||||
// (on macOS /var/folders -> /private/var/folders via symlink)
|
||||
projectDir, err := filepath.EvalSymlinks(projectDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
docDir := filepath.Join(projectDir, ".doc")
|
||||
if err := os.MkdirAll(docDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// set cwd to the project config dir (.doc/) so that:
|
||||
// candidate 2 = projectRoot/.doc/workflow.yaml
|
||||
// candidate 3 = cwd/workflow.yaml = projectRoot/.doc/workflow.yaml (same abs path)
|
||||
originalDir, _ := os.Getwd()
|
||||
t.Cleanup(func() { _ = os.Chdir(originalDir) })
|
||||
_ = os.Chdir(docDir)
|
||||
|
||||
ResetPathManager()
|
||||
pm := mustGetPathManager()
|
||||
pm.projectRoot = projectDir
|
||||
|
||||
writeTriggerFile(t, docDir, `triggers:
|
||||
- description: "doc trigger"
|
||||
ruki: 'before update deny "doc"'
|
||||
`)
|
||||
|
||||
defs, err := LoadTriggerDefs()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// the file should be read exactly once despite two candidates resolving to it
|
||||
if len(defs) != 1 {
|
||||
t.Fatalf("expected 1 def (deduped), got %d", len(defs))
|
||||
}
|
||||
if defs[0].Description != "doc trigger" {
|
||||
t.Errorf("expected 'doc trigger', got %q", defs[0].Description)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -707,6 +707,29 @@ func TestRunQueryUserFunction(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRunQueryUserFunctionViaRunQuery exercises the user() closure inside RunQuery
|
||||
// (not RunSelectQuery). The closure at line 32 captures the resolved user name.
|
||||
func TestRunQueryUserFunctionViaRunQuery(t *testing.T) {
|
||||
s := setupRunnerTest(t)
|
||||
// InMemoryStore.GetCurrentUser returns "memory-user"
|
||||
_ = s.CreateTask(&task.Task{ID: "TIKI-CCC003", Title: "Owned", Status: "ready", Assignee: "memory-user"})
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := RunQuery(gateFor(s), `select id where assignee = user()`, &buf)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "TIKI-CCC003") {
|
||||
t.Errorf("expected TIKI-CCC003 in output:\n%s", out)
|
||||
}
|
||||
// tasks without the matching assignee should be filtered out
|
||||
if strings.Contains(out, "TIKI-AAA001") {
|
||||
t.Errorf("TIKI-AAA001 should be filtered out:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunQueryDeleteValidatorRejection(t *testing.T) {
|
||||
s := setupRunnerTest(t)
|
||||
|
||||
|
|
|
|||
150
ruki/executor_runtime_test.go
Normal file
150
ruki/executor_runtime_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package ruki
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExecutorRuntimeNormalize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode ExecutorRuntimeMode
|
||||
expected ExecutorRuntimeMode
|
||||
}{
|
||||
{"empty mode defaults to cli", "", ExecutorRuntimeCLI},
|
||||
{"cli preserved", ExecutorRuntimeCLI, ExecutorRuntimeCLI},
|
||||
{"plugin preserved", ExecutorRuntimePlugin, ExecutorRuntimePlugin},
|
||||
{"event trigger preserved", ExecutorRuntimeEventTrigger, ExecutorRuntimeEventTrigger},
|
||||
{"time trigger preserved", ExecutorRuntimeTimeTrigger, ExecutorRuntimeTimeTrigger},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := ExecutorRuntime{Mode: tt.mode}
|
||||
got := r.normalize()
|
||||
if got.Mode != tt.expected {
|
||||
t.Errorf("normalize().Mode = %q, want %q", got.Mode, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecutorRuntimeNormalizeDoesNotMutateReceiver verifies normalize returns
|
||||
// a copy and leaves the original unchanged.
|
||||
func TestExecutorRuntimeNormalizeDoesNotMutateReceiver(t *testing.T) {
|
||||
r := ExecutorRuntime{Mode: ""}
|
||||
normalized := r.normalize()
|
||||
if r.Mode != "" {
|
||||
t.Errorf("original mutated: Mode = %q, want %q", r.Mode, "")
|
||||
}
|
||||
if normalized.Mode != ExecutorRuntimeCLI {
|
||||
t.Errorf("normalized.Mode = %q, want %q", normalized.Mode, ExecutorRuntimeCLI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeMismatchErrorMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
validatedFor ExecutorRuntimeMode
|
||||
runtime ExecutorRuntimeMode
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"plugin vs cli",
|
||||
ExecutorRuntimePlugin,
|
||||
ExecutorRuntimeCLI,
|
||||
`validated runtime "plugin" does not match executor runtime "cli"`,
|
||||
},
|
||||
{
|
||||
"event trigger vs time trigger",
|
||||
ExecutorRuntimeEventTrigger,
|
||||
ExecutorRuntimeTimeTrigger,
|
||||
`validated runtime "eventTrigger" does not match executor runtime "timeTrigger"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &RuntimeMismatchError{
|
||||
ValidatedFor: tt.validatedFor,
|
||||
Runtime: tt.runtime,
|
||||
}
|
||||
if got := err.Error(); got != tt.want {
|
||||
t.Errorf("Error() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeMismatchErrorUnwrap(t *testing.T) {
|
||||
err := &RuntimeMismatchError{
|
||||
ValidatedFor: ExecutorRuntimePlugin,
|
||||
Runtime: ExecutorRuntimeCLI,
|
||||
}
|
||||
|
||||
if unwrapped := err.Unwrap(); unwrapped != ErrRuntimeMismatch {
|
||||
t.Errorf("Unwrap() = %v, want %v", unwrapped, ErrRuntimeMismatch)
|
||||
}
|
||||
|
||||
if !errors.Is(err, ErrRuntimeMismatch) {
|
||||
t.Error("errors.Is(err, ErrRuntimeMismatch) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingSelectedTaskIDErrorMessage(t *testing.T) {
|
||||
err := &MissingSelectedTaskIDError{}
|
||||
want := "selected task id is required for plugin runtime when id() is used"
|
||||
if got := err.Error(); got != want {
|
||||
t.Errorf("Error() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCreateTemplateErrorMessage(t *testing.T) {
|
||||
err := &MissingCreateTemplateError{}
|
||||
want := "create template is required for create execution"
|
||||
if got := err.Error(); got != want {
|
||||
t.Errorf("Error() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorTypesWithErrorsAs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
// check runs errors.As against the appropriate target type
|
||||
check func(error) bool
|
||||
}{
|
||||
{
|
||||
"RuntimeMismatchError",
|
||||
&RuntimeMismatchError{ValidatedFor: ExecutorRuntimeCLI, Runtime: ExecutorRuntimePlugin},
|
||||
func(err error) bool {
|
||||
var target *RuntimeMismatchError
|
||||
return errors.As(err, &target)
|
||||
},
|
||||
},
|
||||
{
|
||||
"MissingSelectedTaskIDError",
|
||||
&MissingSelectedTaskIDError{},
|
||||
func(err error) bool {
|
||||
var target *MissingSelectedTaskIDError
|
||||
return errors.As(err, &target)
|
||||
},
|
||||
},
|
||||
{
|
||||
"MissingCreateTemplateError",
|
||||
&MissingCreateTemplateError{},
|
||||
func(err error) bool {
|
||||
var target *MissingCreateTemplateError
|
||||
return errors.As(err, &target)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.check(tt.err) {
|
||||
t.Errorf("errors.As failed for %T", tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -3594,3 +3594,147 @@ func TestCompareValues_BoolDispatch(t *testing.T) {
|
|||
t.Error("true != false should be true via compareValues")
|
||||
}
|
||||
}
|
||||
|
||||
// --- evalID in plugin runtime ---
|
||||
|
||||
func TestExecuteEvalIDPluginRuntime(t *testing.T) {
|
||||
p := newTestParser()
|
||||
e := NewExecutor(testSchema{}, func() string { return "alice" }, ExecutorRuntime{Mode: ExecutorRuntimePlugin})
|
||||
tasks := makeTasks()
|
||||
|
||||
// id() returns the selected task ID as a constant; comparing it to a
|
||||
// matching literal yields true for all tasks (global predicate).
|
||||
validated, err := p.ParseAndValidateStatement(`select where id() = "TIKI-000001"`, ExecutorRuntimePlugin)
|
||||
if err != nil {
|
||||
t.Fatalf("validate: %v", err)
|
||||
}
|
||||
result, err := e.Execute(validated, tasks, ExecutionInput{SelectedTaskID: "TIKI-000001"})
|
||||
if err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
if result.Select == nil {
|
||||
t.Fatal("expected Select result")
|
||||
}
|
||||
// id() = "TIKI-000001" is always true when selected task is TIKI-000001
|
||||
if len(result.Select.Tasks) != 4 {
|
||||
t.Fatalf("expected 4 tasks (global true predicate), got %d", len(result.Select.Tasks))
|
||||
}
|
||||
|
||||
// compare id() against each task's own id field to select only the matching task
|
||||
validated2, err := p.ParseAndValidateStatement(`select where id = id()`, ExecutorRuntimePlugin)
|
||||
if err != nil {
|
||||
t.Fatalf("validate: %v", err)
|
||||
}
|
||||
result2, err := e.Execute(validated2, tasks, ExecutionInput{SelectedTaskID: "TIKI-000001"})
|
||||
if err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
if len(result2.Select.Tasks) != 1 {
|
||||
t.Fatalf("expected 1 task, got %d", len(result2.Select.Tasks))
|
||||
}
|
||||
if result2.Select.Tasks[0].ID != "TIKI-000001" {
|
||||
t.Errorf("expected TIKI-000001, got %s", result2.Select.Tasks[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteEvalIDPluginRuntimeNoMatch(t *testing.T) {
|
||||
p := newTestParser()
|
||||
e := NewExecutor(testSchema{}, func() string { return "alice" }, ExecutorRuntime{Mode: ExecutorRuntimePlugin})
|
||||
tasks := makeTasks()
|
||||
|
||||
// id() returns "TIKI-000002", compare with literal "TIKI-000001" → always false
|
||||
validated, err := p.ParseAndValidateStatement(`select where id() = "TIKI-000001"`, ExecutorRuntimePlugin)
|
||||
if err != nil {
|
||||
t.Fatalf("validate: %v", err)
|
||||
}
|
||||
result, err := e.Execute(validated, tasks, ExecutionInput{SelectedTaskID: "TIKI-000002"})
|
||||
if err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
if len(result.Select.Tasks) != 0 {
|
||||
t.Fatalf("expected 0 tasks, got %d", len(result.Select.Tasks))
|
||||
}
|
||||
}
|
||||
|
||||
// --- setField type mismatches for description, status, type with unusual value types ---
|
||||
|
||||
func TestSetFieldDescriptionNonString(t *testing.T) {
|
||||
e := newTestExecutor()
|
||||
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Description: "old"}
|
||||
|
||||
err := e.setField(tk, "description", 42)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-string description")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "description must be a string") {
|
||||
t.Errorf("expected 'description must be a string' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFieldStatusWithTaskStatusValue(t *testing.T) {
|
||||
e := newTestExecutor()
|
||||
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
|
||||
|
||||
// pass task.Status("done") directly, not a string
|
||||
err := e.setField(tk, "status", task.Status("done"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if tk.Status != "done" {
|
||||
t.Errorf("expected status 'done', got %q", tk.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFieldTypeWithTaskTypeValue(t *testing.T) {
|
||||
e := newTestExecutor()
|
||||
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug"}
|
||||
|
||||
// pass task.Type("story") directly, not a string
|
||||
err := e.setField(tk, "type", task.Type("story"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if tk.Type != "story" {
|
||||
t.Errorf("expected type 'story', got %q", tk.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFieldStatusWithIntValue(t *testing.T) {
|
||||
e := newTestExecutor()
|
||||
tk := &task.Task{ID: "T1", Title: "x", Status: "ready"}
|
||||
|
||||
err := e.setField(tk, "status", 42)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for int status")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "status must be a string") {
|
||||
t.Errorf("expected 'status must be a string' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetFieldTypeWithIntValue(t *testing.T) {
|
||||
e := newTestExecutor()
|
||||
tk := &task.Task{ID: "T1", Title: "x", Status: "ready", Type: "bug"}
|
||||
|
||||
err := e.setField(tk, "type", 42)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for int type")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "type must be a string") {
|
||||
t.Errorf("expected 'type must be a string' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Execute with unsupported statement type ---
|
||||
|
||||
func TestExecuteUnsupportedStatementType(t *testing.T) {
|
||||
e := newTestExecutor()
|
||||
|
||||
_, err := e.Execute("not a statement", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unsupported statement type")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported statement type") {
|
||||
t.Errorf("expected 'unsupported statement type' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2408
ruki/semantic_validate_test.go
Normal file
2408
ruki/semantic_validate_test.go
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue