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
65 changes: 65 additions & 0 deletions cmd/kai/bridge_default_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestAutoImportEnabled_DefaultTrue(t *testing.T) {
chdirTemp(t)
oldKaiDir := kaiDir
kaiDir = ".kai"
defer func() { kaiDir = oldKaiDir }()

// No config at all → the git→kai direction defaults on.
if !autoImportEnabled() {
t.Fatal("autoImportEnabled() false with no config; default must be true")
}

// Explicit opt-out wins.
if err := os.MkdirAll(kaiDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(kaiDir, "config.yaml"), []byte("bridge:\n auto_import: false\n"), 0644); err != nil {
t.Fatal(err)
}
if autoImportEnabled() {
t.Fatal("autoImportEnabled() true despite bridge.auto_import: false")
}
}

// TestImportHookScripts_RunInBackground pins the default-on latency
// guarantee: every import hook backgrounds its kai invocation so git
// returns immediately. A synchronous `kai bridge import` in a default
// hook would put a capture on every commit's critical path.
func TestImportHookScripts_RunInBackground(t *testing.T) {
for name, s := range map[string]string{
"postCommit": postCommitHookScript,
"postMerge": postMergeHookScript,
"postCheckout": postCheckoutHookScript,
"postRewrite": postRewriteHookScript,
} {
if !strings.Contains(s, ") &") {
t.Errorf("%s hook runs its import synchronously (no backgrounded subshell)", name)
}
}
// The pre-* hooks must NOT background anything — they exist to gate.
for name, s := range map[string]string{
"preCommit": preCommitHookScript,
"prePush": prePushHookScript,
} {
if strings.Contains(s, ") &") {
t.Errorf("%s hook backgrounds work; pre-hooks must stay synchronous", name)
}
}
}

// The post-rewrite hook reads stdin, which git closes when the hook
// process exits — the script must buffer stdin before backgrounding.
func TestPostRewriteHookScript_BuffersStdinBeforeBackgrounding(t *testing.T) {
if !strings.Contains(postRewriteHookScript, "PAIRS=$(cat)") {
t.Fatal("post-rewrite hook must buffer stdin before the backgrounded subshell")
}
}
57 changes: 52 additions & 5 deletions cmd/kai/bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func TestHookScripts_AllContainReEntrancyGuard(t *testing.T) {
"postCommit": postCommitHookScript,
"postMerge": postMergeHookScript,
"postCheckout": postCheckoutHookScript,
"postRewrite": postRewriteHookScript,
}
for name, s := range scripts {
if !strings.Contains(s, `"${KAI_BRIDGE_INPROGRESS:-}" = "1"`) {
Expand All @@ -90,6 +91,7 @@ func TestHookScripts_AllTaggedWithCurrentVersion(t *testing.T) {
"postCommit": postCommitHookScript,
"postMerge": postMergeHookScript,
"postCheckout": postCheckoutHookScript,
"postRewrite": postRewriteHookScript,
} {
if !strings.Contains(s, tag) {
t.Errorf("%s hook missing version tag %q", name, tag)
Expand Down Expand Up @@ -198,12 +200,17 @@ func setupBridgeRepo(t *testing.T) {
}
}

func TestBridgeImport_NoOpWhenBridgeDisabled(t *testing.T) {
func TestBridgeImport_NoOpWhenBothDirectionsOff(t *testing.T) {
setupBridgeRepo(t)
// Remove sentinel: disabled
// The import runs by default (bridge.auto_import) even without the
// milestone sentinel — turning it off requires BOTH: no sentinel AND
// an explicit auto_import opt-out.
if err := os.Remove(filepath.Join(".kai", "bridge-enabled")); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(".kai", "config.yaml"), []byte("bridge:\n auto_import: false\n"), 0644); err != nil {
t.Fatal(err)
}
sha := gitHeadSHA(t)
if err := runBridgeImport(nil, []string{sha}); err != nil {
t.Fatalf("import returned error: %v", err)
Expand All @@ -215,6 +222,22 @@ func TestBridgeImport_NoOpWhenBridgeDisabled(t *testing.T) {
}
}

func TestBridgeImport_RunsByDefaultWithoutSentinel(t *testing.T) {
setupBridgeRepo(t)
// No sentinel, no config: auto_import defaults on, so the import
// proceeds — this is what keeps the graph from drifting by default.
if err := os.Remove(filepath.Join(".kai", "bridge-enabled")); err != nil {
t.Fatal(err)
}
sha := gitHeadSHA(t)
if err := runBridgeImport(nil, []string{sha}); err != nil {
t.Fatalf("import returned error: %v", err)
}
if !hasAnyGitRef(t) {
t.Fatal("default auto_import should have imported the commit")
}
}

func TestBridgeImport_CreatesGitRef(t *testing.T) {
setupBridgeRepo(t)
sha := gitHeadSHA(t)
Expand Down Expand Up @@ -310,9 +333,10 @@ func TestBridgeImport_HandlesUnknownSHA(t *testing.T) {
}
}

func TestHookInstall_SkipsPostCommitWhenBridgeDisabled(t *testing.T) {
func TestHookInstall_InstallsImportHooksByDefault(t *testing.T) {
chdirTemp(t)
// git repo present, but no bridge sentinel.
// git repo present, no bridge sentinel, no config: the git→kai import
// hooks install by default.
if err := os.MkdirAll(filepath.Join(".git", "hooks"), 0755); err != nil {
t.Fatal(err)
}
Expand All @@ -321,8 +345,31 @@ func TestHookInstall_SkipsPostCommitWhenBridgeDisabled(t *testing.T) {
if err := runHookInstall(nil, nil); err != nil {
t.Fatalf("hook install: %v", err)
}
for _, name := range []string{"pre-commit", "post-commit", "post-merge", "post-checkout", "post-rewrite"} {
if _, err := os.Stat(filepath.Join(".git", "hooks", name)); err != nil {
t.Errorf("%s hook was not installed by default", name)
}
}
}

func TestHookInstall_SkipsImportHooksWhenAutoImportOff(t *testing.T) {
chdirTemp(t)
if err := os.MkdirAll(filepath.Join(".git", "hooks"), 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(".kai", 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(".kai", "config.yaml"), []byte("bridge:\n auto_import: false\n"), 0644); err != nil {
t.Fatal(err)
}
initMode = true
defer func() { initMode = false }()
if err := runHookInstall(nil, nil); err != nil {
t.Fatalf("hook install: %v", err)
}
if _, err := os.Stat(filepath.Join(".git", "hooks", "post-commit")); err == nil {
t.Fatal("post-commit hook was installed even though bridge is disabled")
t.Fatal("post-commit hook installed despite bridge.auto_import: false")
}
// pre-commit should still be installed.
if _, err := os.Stat(filepath.Join(".git", "hooks", "pre-commit")); err != nil {
Expand Down
30 changes: 20 additions & 10 deletions cmd/kai/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,9 @@ func TestShortID(t *testing.T) {
// TestCommandFlags tests that commands have expected flags
func TestCommandFlags(t *testing.T) {
tests := []struct {
cmd *cobra.Command
flags []string
cmdName string
cmd *cobra.Command
flags []string
cmdName string
}{
{snapshotCreateCmd, []string{"repo", "dir", "message"}, "snapshot create"},
{statusCmd, []string{"dir", "against", "name-only", "json", "semantic"}, "status"},
Expand Down Expand Up @@ -404,6 +404,12 @@ func TestRunInit_CreatesKaiDirectory(t *testing.T) {
os.Chdir(tmpDir)
defer os.Chdir(oldWd)

// Local graph only — a logged-in developer's suite run must never
// sign up repos on kaicontext.com.
oldNoRemote := initNoRemote
initNoRemote = true
defer func() { initNoRemote = oldNoRemote }()

// Run init
err := runInit(initCmd, nil)
if err != nil {
Expand Down Expand Up @@ -449,6 +455,10 @@ func TestRunInit_Idempotent(t *testing.T) {
os.Chdir(tmpDir)
defer os.Chdir(oldWd)

oldNoRemote := initNoRemote
initNoRemote = true
defer func() { initNoRemote = oldNoRemote }()

// Run init twice
if err := runInit(initCmd, nil); err != nil {
t.Fatalf("first runInit failed: %v", err)
Expand Down Expand Up @@ -484,8 +494,8 @@ func TestCommandHelp(t *testing.T) {
func TestSubcommandArgs(t *testing.T) {
// Commands that require exact args
exactArgsCommands := []struct {
cmd *cobra.Command
args int
cmd *cobra.Command
args int
}{
{analyzeSymbolsCmd, 1},
{changesetCreateCmd, 2},
Expand Down Expand Up @@ -732,11 +742,11 @@ func TestFlagsHaveDefaults(t *testing.T) {
// TestShowUnifiedDiff tests the pure Go unified diff implementation
func TestShowUnifiedDiff(t *testing.T) {
tests := []struct {
name string
before string
after string
wantAdd bool
wantDel bool
name string
before string
after string
wantAdd bool
wantDel bool
}{
{
name: "single line addition",
Expand Down
4 changes: 4 additions & 0 deletions cmd/kai/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func TestRunInitAlreadyInitialized(t *testing.T) {
initForce = false
defer func() { initForce = oldInitForce }()

oldNoRemote := initNoRemote
initNoRemote = true
defer func() { initNoRemote = oldNoRemote }()

r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
Expand Down
Loading