Files
windmill/cli/test/workspace_conflicts_unit.test.ts
centdix 5fd2c1a129 chore(cli): separate unit tests from integration tests and fix test cleanup (#8562)
* fix(cli): separate unit tests from integration tests and fix test cleanup

- Rename 14 non-backend test files to *_unit.test.ts convention
- Add UNIT_ONLY env var guard in setup.ts to skip cargo build/backend startup
- Add test:unit and test:integration scripts to package.json
- Use setsid on Linux for process group management so stop() kills both
  cargo and the windmill child process
- Fix exit handler to kill process group instead of just the direct child
- Add cleanupStaleTestResources() to drop orphaned windmill_test_* databases
  and kill orphaned backend processes on startup
- Rewrite TESTING.md with current bun-based instructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): fix process group approach - kill by db name instead of setsid

The setsid approach didn't work because setsid forks, making the PID
we get from Bun.spawn ephemeral. Instead, kill orphaned windmill child
processes by matching our unique database name in /proc/pid/environ.

Also add afterAll hook in setup.ts so full async cleanup (process kill
+ database drop) runs when all tests complete normally, not just on
SIGINT/SIGTERM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): address PR review feedback

- Remove duplicate cleanupStaleTestResources() call in getTestBackend()
  (already called in setup.ts)
- Add regex guard on database names before SQL interpolation
- Extract shared killWindmillProcessesByEnvMatch() helper to deduplicate
  process-killing logic
- Remove redundant test:integration script (test already runs everything)
- Flip setup.ts to if/else pattern for readability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:13:33 +00:00

200 lines
6.8 KiB
TypeScript

import { expect, test } from "bun:test";
import { addWorkspace, allWorkspaces } from "../workspace.ts";
import { withTestConfig, clearTestRemotes } from "./test_config_helpers.ts";
// Test workspace conflict detection
test("addWorkspace: prevents duplicate workspace names", async () => {
await withTestConfig(async (testConfigDir) => {
await clearTestRemotes(testConfigDir);
// Add first workspace
const workspace1 = {
name: "test_workspace",
remote: "http://localhost:8001/",
workspaceId: "workspace1",
token: "token1"
};
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
// Try to add workspace with same name but different details
const workspace2 = {
name: "test_workspace", // Same name
remote: "http://localhost:8002/", // Different remote
workspaceId: "workspace2", // Different ID
token: "token2"
};
// Force non-interactive mode so addWorkspace throws instead of prompting
const origStdinTTY = process.stdin.isTTY;
const origStdoutTTY = process.stdout.isTTY;
try {
process.stdin.isTTY = false as any;
process.stdout.isTTY = false as any;
// Should throw error in non-interactive mode without force
await expect(
addWorkspace(workspace2, { configDir: testConfigDir })
).rejects.toThrow("Workspace name conflict. Use --force to overwrite or choose a different name.");
} finally {
process.stdin.isTTY = origStdinTTY;
process.stdout.isTTY = origStdoutTTY;
}
// Should succeed with force flag
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
// Verify the workspace was overwritten
const workspaces = await allWorkspaces(testConfigDir);
expect(workspaces.length).toEqual(1);
expect(workspaces[0].name).toEqual("test_workspace");
expect(workspaces[0].remote).toEqual("http://localhost:8002/");
expect(workspaces[0].workspaceId).toEqual("workspace2");
});
});
test("addWorkspace: prevents duplicate (remote, workspaceId) tuples", async () => {
await withTestConfig(async (testConfigDir) => {
await clearTestRemotes(testConfigDir);
// Add first workspace
const workspace1 = {
name: "first_workspace",
remote: "http://localhost:8001/",
workspaceId: "test",
token: "token1"
};
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
// Try to add workspace with same (remote, workspaceId) but different name
const workspace2 = {
name: "second_workspace", // Different name
remote: "http://localhost:8001/", // Same remote
workspaceId: "test", // Same workspaceId
token: "token2"
};
// Should throw error in non-interactive mode without force
await expect(
addWorkspace(workspace2, { configDir: testConfigDir })
).rejects.toThrow('Backend constraint violation: (http://localhost:8001/, test) already exists as "first_workspace". Use --force to overwrite.');
// Should succeed with force flag (overwrites first workspace)
await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
// Verify the first workspace was removed and second was added
const workspaces = await allWorkspaces(testConfigDir);
expect(workspaces.length).toEqual(1);
expect(workspaces[0].name).toEqual("second_workspace");
expect(workspaces[0].remote).toEqual("http://localhost:8001/");
expect(workspaces[0].workspaceId).toEqual("test");
});
});
test("addWorkspace: allows same workspace (name, remote, workspaceId) with token update", async () => {
await withTestConfig(async (testConfigDir) => {
await clearTestRemotes(testConfigDir);
// Add first workspace
const workspace1 = {
name: "same_workspace",
remote: "http://localhost:8001/",
workspaceId: "test",
token: "old_token"
};
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
// Add same workspace with updated token
const workspace2 = {
name: "same_workspace", // Same name
remote: "http://localhost:8001/", // Same remote
workspaceId: "test", // Same workspaceId
token: "new_token" // Different token
};
// Should succeed without force (just token update)
await addWorkspace(workspace2, { configDir: testConfigDir });
// Verify token was updated
const workspaces = await allWorkspaces(testConfigDir);
expect(workspaces.length).toEqual(1);
expect(workspaces[0].name).toEqual("same_workspace");
expect(workspaces[0].token).toEqual("new_token");
});
});
test("addWorkspace: returns true on successful add", async () => {
await withTestConfig(async (testConfigDir) => {
await clearTestRemotes(testConfigDir);
const workspace = {
name: "return_test",
remote: "http://localhost:8001/",
workspaceId: "test",
token: "token1"
};
const result = await addWorkspace(workspace, { force: true, configDir: testConfigDir });
expect(result).toEqual(true);
});
});
test("addWorkspace: returns true when force-overwriting conflict", async () => {
await withTestConfig(async (testConfigDir) => {
await clearTestRemotes(testConfigDir);
const workspace1 = {
name: "force_test",
remote: "http://localhost:8001/",
workspaceId: "workspace1",
token: "token1"
};
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
const workspace2 = {
name: "force_test",
remote: "http://localhost:8002/",
workspaceId: "workspace2",
token: "token2"
};
const result = await addWorkspace(workspace2, { force: true, configDir: testConfigDir });
expect(result).toEqual(true);
});
});
test("addWorkspace: allows different workspaces on different remotes", async () => {
await withTestConfig(async (testConfigDir) => {
await clearTestRemotes(testConfigDir);
// Add workspace on first remote
const workspace1 = {
name: "workspace_remote1",
remote: "http://localhost:8001/",
workspaceId: "test",
token: "token1"
};
await addWorkspace(workspace1, { force: true, configDir: testConfigDir });
// Add workspace with same workspaceId on different remote (should be allowed)
const workspace2 = {
name: "workspace_remote2",
remote: "http://localhost:8002/", // Different remote
workspaceId: "test", // Same workspaceId (OK on different remote)
token: "token2"
};
// Should succeed (different remotes)
await addWorkspace(workspace2, { configDir: testConfigDir });
// Verify both workspaces exist
const workspaces = await allWorkspaces(testConfigDir);
expect(workspaces.length).toEqual(2);
const names = workspaces.map(w => w.name).sort();
expect(names).toEqual(["workspace_remote1", "workspace_remote2"]);
});
});