fix(droid): align hooks with Factory 0.175
This commit is contained in:
1 parent
827374e7f8
commit
4521cbe32f
15 files changed
+2749
-881
No files matched your search
@@ -0,0 +1,270 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func prepareDroidTestInstallation(
|
||||
t *testing.T,
|
||||
fixture *droidFixture,
|
||||
config InstallConfig,
|
||||
) *preparedDroidInstallation {
|
||||
t.Helper()
|
||||
sources, err := readDroidSources()
|
||||
if err != nil {
|
||||
t.Fatalf("read Factory Droid sources: %v", err)
|
||||
}
|
||||
paths, nodePath, err := preflightDroidInstallation(config, sources)
|
||||
if err != nil {
|
||||
t.Fatalf("preflight Factory Droid installation: %v", err)
|
||||
}
|
||||
rendered, err := renderDroidInstallation(
|
||||
sources,
|
||||
paths.guardPath,
|
||||
paths.auditPath,
|
||||
nodePath,
|
||||
paths.runtimeRoot,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("render Factory Droid installation: %v", err)
|
||||
}
|
||||
prepared, err := prepareDroidInstallation(config, sources, rendered)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare Factory Droid installation: %v", err)
|
||||
}
|
||||
return prepared
|
||||
}
|
||||
|
||||
func droidManagedPaths(fixture *droidFixture) []string {
|
||||
return []string{
|
||||
fixture.guardPath,
|
||||
fixture.runtimeConfig,
|
||||
fixture.privateKey,
|
||||
fixture.hookPath,
|
||||
fixture.configPath,
|
||||
}
|
||||
}
|
||||
|
||||
func TestDroidInstallRollsBackRuntimeAfterLateHookCommitFailure(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{})
|
||||
prepared := prepareDroidTestInstallation(t, fixture, fixture.config)
|
||||
failed := false
|
||||
rename := func(source, destination string) error {
|
||||
if !failed && sameDroidPath(destination, fixture.configPath) {
|
||||
failed = true
|
||||
return errors.New("injected Droid hook commit failure")
|
||||
}
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
err := commitDroidInstallation(prepared, rename)
|
||||
if err == nil || !strings.Contains(err.Error(), "injected Droid hook commit failure") || !failed {
|
||||
t.Fatalf("transaction error = %v, failed = %v", err, failed)
|
||||
}
|
||||
for _, path := range droidManagedPaths(fixture) {
|
||||
requireMissingDroidFile(t, path)
|
||||
}
|
||||
requireNoDroidStagingFiles(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestDroidConcurrentActiveHookReplacementPreventsRuntimeCommit(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{})
|
||||
installDroidFixture(t, fixture)
|
||||
before := snapshotDroidFiles(t, droidManagedPaths(fixture)...)
|
||||
updated := fixture.config
|
||||
updated.OrgID = "org-updated"
|
||||
prepared := prepareDroidTestInstallation(t, fixture, updated)
|
||||
concurrent := "{\"hooks\":{\"PreToolUse\":[]},\"owner\":\"concurrent\"}\n"
|
||||
if err := os.WriteFile(fixture.configPath, []byte(concurrent), 0600); err != nil {
|
||||
t.Fatalf("replace active hook source: %v", err)
|
||||
}
|
||||
err := commitDroidInstallation(prepared, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "changed during Install Factory Droid") {
|
||||
t.Fatalf("concurrent source error = %v", err)
|
||||
}
|
||||
for path, source := range before {
|
||||
if sameDroidPath(path, fixture.configPath) {
|
||||
continue
|
||||
}
|
||||
if readDroidTestFile(t, path) != source {
|
||||
t.Fatalf("runtime changed after concurrent source replacement: %s", path)
|
||||
}
|
||||
}
|
||||
if readDroidTestFile(t, fixture.configPath) != concurrent {
|
||||
t.Fatal("concurrent source was overwritten")
|
||||
}
|
||||
requireNoDroidStagingFiles(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestDroidConcurrentInactiveLocalSourceCreationIsDetected(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{})
|
||||
installDroidFixture(t, fixture)
|
||||
before := snapshotDroidFiles(t, droidManagedPaths(fixture)...)
|
||||
updated := fixture.config
|
||||
updated.OrgID = "org-updated"
|
||||
prepared := prepareDroidTestInstallation(t, fixture, updated)
|
||||
concurrent := "{\"hooksDisabled\":true}\n"
|
||||
writeOptionalDroidFile(t, fixture.localSettingsPath, droidString(concurrent))
|
||||
err := commitDroidInstallation(prepared, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "local settings changed during Install") {
|
||||
t.Fatalf("concurrent local source error = %v", err)
|
||||
}
|
||||
requireDroidSnapshot(t, before)
|
||||
if readDroidTestFile(t, fixture.localSettingsPath) != concurrent {
|
||||
t.Fatal("concurrent local settings source was overwritten")
|
||||
}
|
||||
requireNoDroidStagingFiles(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestDroidConcurrentProjectPolicyChangeIsDetected(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{
|
||||
projectSettings: droidJSON(map[string]any{"hooksDisabled": false}),
|
||||
})
|
||||
installDroidFixture(t, fixture)
|
||||
before := snapshotDroidFiles(t, droidManagedPaths(fixture)...)
|
||||
updated := fixture.config
|
||||
updated.OrgID = "org-updated"
|
||||
prepared := prepareDroidTestInstallation(t, fixture, updated)
|
||||
concurrent := "{\"hooksDisabled\":true}\n"
|
||||
if err := os.WriteFile(fixture.projectSettingsPath, []byte(concurrent), 0600); err != nil {
|
||||
t.Fatalf("change project policy: %v", err)
|
||||
}
|
||||
err := commitDroidInstallation(prepared, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "project settings changed during Install") {
|
||||
t.Fatalf("concurrent project policy error = %v", err)
|
||||
}
|
||||
requireDroidSnapshot(t, before)
|
||||
if readDroidTestFile(t, fixture.projectSettingsPath) != concurrent {
|
||||
t.Fatal("concurrent project policy was overwritten")
|
||||
}
|
||||
requireNoDroidStagingFiles(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestDroidConcurrentPolicyChangePreventsRuntimeDirectoryCreation(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{
|
||||
projectSettings: droidJSON(map[string]any{"hooksDisabled": false}),
|
||||
})
|
||||
prepared := prepareDroidTestInstallation(t, fixture, fixture.config)
|
||||
if err := os.WriteFile(
|
||||
fixture.projectSettingsPath,
|
||||
[]byte("{\"hooksDisabled\":true}\n"),
|
||||
0600,
|
||||
); err != nil {
|
||||
t.Fatalf("change project policy: %v", err)
|
||||
}
|
||||
err := commitDroidInstallation(prepared, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "project settings changed during Install") {
|
||||
t.Fatalf("concurrent project policy error = %v", err)
|
||||
}
|
||||
requireMissingDroidFile(t, fixture.agentDir)
|
||||
requireMissingDroidFile(t, fixture.configPath)
|
||||
}
|
||||
|
||||
func TestDroidSameContentReplacementIsRejectedByPhysicalIdentity(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{})
|
||||
installDroidFixture(t, fixture)
|
||||
before := snapshotDroidFiles(t, droidManagedPaths(fixture)...)
|
||||
sources, err := readDroidSources()
|
||||
if err != nil {
|
||||
t.Fatalf("read Factory Droid sources: %v", err)
|
||||
}
|
||||
original := readDroidTestFile(t, fixture.configPath)
|
||||
if err := os.Remove(fixture.configPath); err != nil {
|
||||
t.Fatalf("remove hook source: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(fixture.configPath, []byte(original), 0600); err != nil {
|
||||
t.Fatalf("replace hook source: %v", err)
|
||||
}
|
||||
updated := fixture.config
|
||||
updated.OrgID = "org-updated"
|
||||
paths, nodePath, err := preflightDroidInstallation(updated, sources)
|
||||
if err != nil {
|
||||
t.Fatalf("preflight stale sources: %v", err)
|
||||
}
|
||||
rendered, err := renderDroidInstallation(
|
||||
sources,
|
||||
paths.guardPath,
|
||||
paths.auditPath,
|
||||
nodePath,
|
||||
paths.runtimeRoot,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("render stale sources: %v", err)
|
||||
}
|
||||
prepared, err := prepareDroidInstallation(updated, sources, rendered)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare stale sources: %v", err)
|
||||
}
|
||||
err = commitDroidInstallation(prepared, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "changed during Install") {
|
||||
t.Fatalf("same-content replacement error = %v", err)
|
||||
}
|
||||
if readDroidTestFile(t, fixture.configPath) != original {
|
||||
t.Fatal("same-content replacement changed")
|
||||
}
|
||||
for path, source := range before {
|
||||
if !sameDroidPath(path, fixture.configPath) && readDroidTestFile(t, path) != source {
|
||||
t.Fatalf("runtime changed after inode replacement: %s", path)
|
||||
}
|
||||
}
|
||||
requireNoDroidStagingFiles(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestDroidUninstallRollsBackAllHookDocuments(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{})
|
||||
installDroidFixture(t, fixture)
|
||||
installed := droidCurrentHooks(t, fixture.configPath)
|
||||
writeDroidTestObject(t, fixture.settingsPath, map[string]any{
|
||||
"hooks": installed,
|
||||
"owner": "user",
|
||||
})
|
||||
paths := append(droidManagedPaths(fixture), fixture.settingsPath)
|
||||
before := snapshotDroidFiles(t, paths...)
|
||||
sources, err := readDroidSources()
|
||||
if err != nil {
|
||||
t.Fatalf("read uninstall sources: %v", err)
|
||||
}
|
||||
runtimeRoot, err := droidRuntimeRoot()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve runtime root: %v", err)
|
||||
}
|
||||
rendered, err := renderDroidUninstall(sources, droidTestAgentID, runtimeRoot)
|
||||
if err != nil {
|
||||
t.Fatalf("render uninstall: %v", err)
|
||||
}
|
||||
prepared, err := prepareDroidUninstall(rendered)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare uninstall: %v", err)
|
||||
}
|
||||
failed := false
|
||||
rename := func(source, destination string) error {
|
||||
if !failed && sameDroidPath(destination, fixture.settingsPath) {
|
||||
failed = true
|
||||
return errors.New("injected Droid uninstall failure")
|
||||
}
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
err = commitDroidUninstall(prepared, rename)
|
||||
if err == nil || !strings.Contains(err.Error(), "injected Droid uninstall failure") || !failed {
|
||||
t.Fatalf("uninstall rollback error = %v, failed = %v", err, failed)
|
||||
}
|
||||
requireDroidSnapshot(t, before)
|
||||
requireNoDroidStagingFiles(t, fixture.homeDir)
|
||||
}
|
||||
|
||||
func TestDroidPreflightBlocksDisabledHooksBeforeRuntimeCreation(t *testing.T) {
|
||||
fixture := prepareDroidFixture(t, droidFixtureOptions{
|
||||
settings: droidJSON(map[string]any{"hooksDisabled": true}),
|
||||
})
|
||||
preflighter, ok := any(fixture.plugin).(InstallPreflighter)
|
||||
if !ok {
|
||||
t.Fatal("Factory Droid plugin does not implement InstallPreflighter")
|
||||
}
|
||||
err := preflighter.PreflightInstall(fixture.config)
|
||||
if err == nil || !strings.Contains(err.Error(), "hooksDisabled") {
|
||||
t.Fatalf("preflight error = %v", err)
|
||||
}
|
||||
requireMissingDroidFile(t, fixture.agentDir)
|
||||
}
|
||||
Reference in new issue
Block a user