* 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>
710 lines
20 KiB
TypeScript
710 lines
20 KiB
TypeScript
/**
|
|
* Unified Test Backend Interface
|
|
*
|
|
* Provides a common interface for both Docker-based and Cargo-based backends.
|
|
* Use environment variable TEST_BACKEND to switch:
|
|
* - TEST_BACKEND=cargo (default) - Uses pre-built binary + local postgres
|
|
* - TEST_BACKEND=docker - Uses docker-compose (legacy)
|
|
*
|
|
* Prerequisites for cargo backend:
|
|
* - PostgreSQL running locally (default: postgres://postgres:changeme@localhost:5432)
|
|
* - Backend built: cd backend && cargo build --release (or debug)
|
|
*
|
|
* Usage:
|
|
* import { withTestBackend, cleanupTestBackend } from "./test_backend.ts";
|
|
*
|
|
* test("my test", async () => {
|
|
* await withTestBackend(async (backend, tempDir) => {
|
|
* const result = await backend.runCLICommand(["sync", "pull"], tempDir);
|
|
* // ...
|
|
* });
|
|
* });
|
|
*
|
|
* CROSS-LINKS - Related test helper locations (keep in sync when adding new helpers):
|
|
* @see test_fixtures.ts - Local file fixtures (createLocalScript, createLocalFlow, etc.)
|
|
* @see sync_pull_push.test.ts - Local fixtures + createRemoteScript (API-based)
|
|
*
|
|
* This file contains: API-based creation helpers (createTestApp, createTestResource,
|
|
* createAppWithInlineScript, createFlowWithInlineScript, etc.)
|
|
* If you add new helpers, update cross-links in the files above.
|
|
*/
|
|
|
|
import { CargoBackend, CargoBackendConfig } from "./cargo_backend.ts";
|
|
import { ContainerizedBackend, ContainerConfig } from "./containerized_backend.ts";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
/**
|
|
* Common interface for test backends
|
|
*/
|
|
export interface TestBackend {
|
|
readonly baseUrl: string;
|
|
readonly workspace: string;
|
|
readonly testConfigDir: string;
|
|
readonly token?: string;
|
|
|
|
start(): Promise<void>;
|
|
stop(): Promise<void>;
|
|
reset(): Promise<void>;
|
|
|
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any;
|
|
runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): Promise<{
|
|
stdout: string;
|
|
stderr: string;
|
|
code: number;
|
|
}>;
|
|
|
|
// Optional methods that may not be on all backends
|
|
apiRequest?(path: string, options?: RequestInit): Promise<Response>;
|
|
seedTestData?(): Promise<void>;
|
|
getWorkspaceSettings?(): Promise<any>;
|
|
updateGitSyncConfig?(config: any): Promise<void>;
|
|
createAdditionalGitRepo?(repoPath: string, description: string): Promise<void>;
|
|
listAllScripts?(): Promise<any[]>;
|
|
listAllApps?(): Promise<any[]>;
|
|
listAllResources?(): Promise<any[]>;
|
|
listAllVariables?(): Promise<any[]>;
|
|
|
|
// Methods for creating apps and flows with custom inline scripts
|
|
createAppWithInlineScript?(path: string, inlineScriptContent: string, language?: string): Promise<void>;
|
|
createFlowWithInlineScript?(path: string, inlineScriptContent: string, language?: string): Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Adapter to make CargoBackend implement TestBackend
|
|
*/
|
|
class CargoBackendAdapter implements TestBackend {
|
|
private backend: CargoBackend;
|
|
|
|
constructor(config?: Partial<CargoBackendConfig>) {
|
|
this.backend = new CargoBackend(config);
|
|
}
|
|
|
|
get baseUrl(): string {
|
|
return this.backend.baseUrl;
|
|
}
|
|
|
|
get workspace(): string {
|
|
return this.backend.workspace;
|
|
}
|
|
|
|
get testConfigDir(): string {
|
|
return this.backend.testConfigDir;
|
|
}
|
|
|
|
get token(): string {
|
|
return this.backend.authToken;
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
await this.backend.start();
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
await this.backend.stop();
|
|
}
|
|
|
|
async reset(): Promise<void> {
|
|
await this.backend.reset();
|
|
}
|
|
|
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any {
|
|
return this.backend.createCLICommand(args, workingDir, opts);
|
|
}
|
|
|
|
async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }) {
|
|
return this.backend.runCLICommand(args, workingDir, opts);
|
|
}
|
|
|
|
async apiRequest(path: string, options?: RequestInit): Promise<Response> {
|
|
return this.backend.apiRequest(path, options);
|
|
}
|
|
|
|
/** Seeds test data via API calls. See file header for cross-links to related helpers. */
|
|
async seedTestData(): Promise<void> {
|
|
// Create test folder first
|
|
await this.createTestFolder("test");
|
|
|
|
// Create test resources and variables
|
|
await this.createTestResource("f/test/my_resource", "Test resource description");
|
|
await this.createTestVariable("f/test/my_variable", "Test variable value");
|
|
|
|
// Create test group
|
|
await this.createTestGroup("test_group");
|
|
|
|
// Create a test app
|
|
await this.createTestApp("f/test/test_dashboard");
|
|
}
|
|
|
|
/** See file header for cross-links to related helpers. */
|
|
private async createTestApp(path: string): Promise<void> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/apps/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path,
|
|
value: {
|
|
type: "app",
|
|
grid: [],
|
|
hiddenInlineScripts: [],
|
|
css: {},
|
|
norefreshbar: false,
|
|
},
|
|
summary: "Test app",
|
|
policy: {
|
|
on_behalf_of: null,
|
|
on_behalf_of_email: null,
|
|
triggerables: {},
|
|
execution_mode: "viewer",
|
|
},
|
|
}),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (!error.includes("already exists")) {
|
|
console.warn(`Warning: Failed to create app ${path}: ${error}`);
|
|
}
|
|
} else {
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
/** See file header for cross-links to related helpers. */
|
|
private async createTestFolder(name: string): Promise<void> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/folders/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (!error.includes("already exists")) {
|
|
console.warn(`Warning: Failed to create folder ${name}: ${error}`);
|
|
}
|
|
} else {
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
/** See file header for cross-links to related helpers. */
|
|
private async createTestGroup(name: string): Promise<void> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/groups/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name, summary: `Test group ${name}` }),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (!error.includes("already exists")) {
|
|
console.warn(`Warning: Failed to create group ${name}: ${error}`);
|
|
}
|
|
} else {
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
|
|
/** See file header for cross-links to related helpers. */
|
|
private async createTestResource(path: string, description: string): Promise<void> {
|
|
// First ensure the folder exists
|
|
const folderPath = path.split("/").slice(0, 2).join("/"); // e.g., "f/test"
|
|
const folderName = folderPath.replace("f/", "");
|
|
|
|
try {
|
|
const folderResponse = await this.backend.apiRequest(`/api/w/${this.workspace}/folders/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ name: folderName }),
|
|
});
|
|
await folderResponse.text(); // Consume response body
|
|
} catch {
|
|
// Folder may already exist
|
|
}
|
|
|
|
const response = await this.backend.apiRequest(
|
|
`/api/w/${this.workspace}/resources/create`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path,
|
|
description,
|
|
resource_type: "any",
|
|
value: { test: "value" },
|
|
}),
|
|
}
|
|
);
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (!error.includes("already exists")) {
|
|
console.warn(`Warning: Failed to create resource ${path}: ${error}`);
|
|
}
|
|
} else {
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
/** See file header for cross-links to related helpers. */
|
|
private async createTestVariable(path: string, value: string): Promise<void> {
|
|
const response = await this.backend.apiRequest(
|
|
`/api/w/${this.workspace}/variables/create`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path,
|
|
value,
|
|
is_secret: false,
|
|
description: "Test variable",
|
|
}),
|
|
}
|
|
);
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (!error.includes("already exists")) {
|
|
console.warn(`Warning: Failed to create variable ${path}: ${error}`);
|
|
}
|
|
} else {
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
async getWorkspaceSettings(): Promise<any> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/workspaces/get_settings`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get workspace settings: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async updateGitSyncConfig(config: any): Promise<void> {
|
|
const response = await this.backend.apiRequest(
|
|
`/api/w/${this.workspace}/workspaces/edit_git_sync_config`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(config),
|
|
}
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update git sync config: ${response.status}`);
|
|
}
|
|
await response.text();
|
|
}
|
|
|
|
async createAdditionalGitRepo(repoPath: string, description: string): Promise<void> {
|
|
const gitRepo = {
|
|
path: repoPath,
|
|
description,
|
|
resource_type: "git_repository",
|
|
value: {
|
|
url: "https://github.com/windmill-labs/windmill-test.git",
|
|
branch: "main",
|
|
token: "",
|
|
},
|
|
};
|
|
|
|
const response = await this.backend.apiRequest(
|
|
`/api/w/${this.workspace}/resources/create`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(gitRepo),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
if (!error.includes("already exists")) {
|
|
console.warn(`Failed to create git repo ${repoPath}: ${error}`);
|
|
}
|
|
} else {
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
async listAllScripts(): Promise<any[]> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/scripts/list`);
|
|
if (!response.ok) return [];
|
|
return response.json();
|
|
}
|
|
|
|
async listAllApps(): Promise<any[]> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/apps/list`);
|
|
if (!response.ok) return [];
|
|
return response.json();
|
|
}
|
|
|
|
async listAllResources(): Promise<any[]> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/resources/list`);
|
|
if (!response.ok) return [];
|
|
return response.json();
|
|
}
|
|
|
|
async listAllVariables(): Promise<any[]> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/variables/list`);
|
|
if (!response.ok) return [];
|
|
return response.json();
|
|
}
|
|
|
|
async createAppWithInlineScript(path: string, inlineScriptContent: string, language: string = "bun"): Promise<void> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/apps/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path,
|
|
value: {
|
|
type: "app",
|
|
grid: [
|
|
{
|
|
id: "button1",
|
|
data: {
|
|
type: "buttoncomponent",
|
|
componentInput: {
|
|
type: "runnable",
|
|
runnable: {
|
|
type: "runnableByName",
|
|
inlineScript: {
|
|
content: inlineScriptContent,
|
|
language,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
hiddenInlineScripts: [],
|
|
css: {},
|
|
norefreshbar: false,
|
|
},
|
|
summary: "Test app with inline script",
|
|
policy: {
|
|
on_behalf_of: null,
|
|
on_behalf_of_email: null,
|
|
triggerables: {},
|
|
execution_mode: "viewer",
|
|
},
|
|
}),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`Failed to create app ${path}: ${error}`);
|
|
}
|
|
await response.text();
|
|
}
|
|
|
|
async createFlowWithInlineScript(path: string, inlineScriptContent: string, language: string = "bun"): Promise<void> {
|
|
const response = await this.backend.apiRequest(`/api/w/${this.workspace}/flows/create`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
path,
|
|
summary: "Test flow with inline script",
|
|
description: `Flow at ${path}`,
|
|
value: {
|
|
modules: [
|
|
{
|
|
id: "a",
|
|
value: {
|
|
type: "rawscript",
|
|
content: inlineScriptContent,
|
|
language,
|
|
input_transforms: {},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
schema: {
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
type: "object",
|
|
properties: {},
|
|
required: [],
|
|
},
|
|
}),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`Failed to create flow ${path}: ${error}`);
|
|
}
|
|
await response.text();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adapter to make ContainerizedBackend implement TestBackend
|
|
*/
|
|
class ContainerizedBackendAdapter implements TestBackend {
|
|
private backend: ContainerizedBackend;
|
|
|
|
constructor(config?: Partial<ContainerConfig>) {
|
|
this.backend = new ContainerizedBackend(config);
|
|
}
|
|
|
|
get baseUrl(): string {
|
|
return this.backend.baseUrl;
|
|
}
|
|
|
|
get workspace(): string {
|
|
return this.backend.workspace;
|
|
}
|
|
|
|
get testConfigDir(): string {
|
|
return this.backend.testConfigDir;
|
|
}
|
|
|
|
get token(): string {
|
|
return this.backend.token;
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
await this.backend.start();
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
await this.backend.stop();
|
|
}
|
|
|
|
async reset(): Promise<void> {
|
|
await this.backend.reset();
|
|
}
|
|
|
|
createCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }): any {
|
|
return this.backend.createCLICommand(args, workingDir, opts);
|
|
}
|
|
|
|
async runCLICommand(args: string[], workingDir: string, opts?: { workspace?: string; token?: string }) {
|
|
return this.backend.runCLICommand(args, workingDir, opts);
|
|
}
|
|
|
|
async seedTestData(): Promise<void> {
|
|
await this.backend.seedTestData();
|
|
}
|
|
|
|
async getWorkspaceSettings(): Promise<any> {
|
|
return this.backend.getWorkspaceSettings();
|
|
}
|
|
|
|
async updateGitSyncConfig(config: any): Promise<void> {
|
|
await this.backend.updateGitSyncConfig(config);
|
|
}
|
|
|
|
async createAdditionalGitRepo(repoPath: string, description: string): Promise<void> {
|
|
await this.backend.createAdditionalGitRepo(repoPath, description);
|
|
}
|
|
|
|
async listAllScripts(): Promise<any[]> {
|
|
return this.backend.listAllScripts();
|
|
}
|
|
|
|
async listAllApps(): Promise<any[]> {
|
|
return this.backend.listAllApps();
|
|
}
|
|
|
|
async listAllResources(): Promise<any[]> {
|
|
return this.backend.listAllResources();
|
|
}
|
|
|
|
async listAllVariables(): Promise<any[]> {
|
|
return this.backend.listAllVariables();
|
|
}
|
|
}
|
|
|
|
// Global backend instance
|
|
let globalBackend: TestBackend | null = null;
|
|
|
|
/**
|
|
* Get the backend type from environment
|
|
*/
|
|
function getBackendType(): "cargo" | "docker" {
|
|
const envType = process.env["TEST_BACKEND"]?.toLowerCase();
|
|
if (envType === "docker") {
|
|
return "docker";
|
|
}
|
|
return "cargo"; // Default to cargo
|
|
}
|
|
|
|
/**
|
|
* Create a new backend instance based on configuration
|
|
*/
|
|
export function createTestBackend(type?: "cargo" | "docker"): TestBackend {
|
|
const backendType = type || getBackendType();
|
|
|
|
if (backendType === "docker") {
|
|
console.log("📦 Using Docker-based test backend");
|
|
return new ContainerizedBackendAdapter();
|
|
} else {
|
|
console.log("🦀 Using Cargo-based test backend");
|
|
return new CargoBackendAdapter({
|
|
verbose: process.env["VERBOSE"] === "1",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or create global backend instance
|
|
*/
|
|
export async function getTestBackend(): Promise<TestBackend> {
|
|
if (!globalBackend) {
|
|
globalBackend = createTestBackend();
|
|
registerCleanup();
|
|
await globalBackend.start();
|
|
}
|
|
return globalBackend;
|
|
}
|
|
|
|
/**
|
|
* Convenience function for tests - runs test with backend
|
|
*/
|
|
export async function withTestBackend<T>(
|
|
testFn: (backend: TestBackend, tempDir: string) => Promise<T>
|
|
): Promise<T> {
|
|
const backend = await getTestBackend();
|
|
const tempDir = await mkdtemp(join(tmpdir(), "windmill_cli_test_"));
|
|
|
|
try {
|
|
await backend.reset();
|
|
if (backend.seedTestData) {
|
|
await backend.seedTestData();
|
|
}
|
|
return await testFn(backend, tempDir);
|
|
} finally {
|
|
await rm(tempDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup function for test suites
|
|
*/
|
|
export async function cleanupTestBackend(): Promise<void> {
|
|
if (globalBackend) {
|
|
await globalBackend.stop();
|
|
globalBackend = null;
|
|
}
|
|
}
|
|
|
|
// Auto-cleanup on process exit
|
|
let cleanupRegistered = false;
|
|
function registerCleanup() {
|
|
if (cleanupRegistered) return;
|
|
cleanupRegistered = true;
|
|
process.on("exit", () => {
|
|
if (globalBackend) {
|
|
// Synchronous kill — can't await in exit handler.
|
|
// Kill the direct child (cargo); any orphaned windmill child processes
|
|
// will be cleaned up by cleanupStaleTestResources() on next startup.
|
|
const pid = (globalBackend as any).backend?.process?.pid;
|
|
if (pid) {
|
|
try {
|
|
process.kill(pid, "SIGKILL");
|
|
} catch {
|
|
// Best effort — process may already be dead
|
|
}
|
|
}
|
|
}
|
|
});
|
|
// Handle graceful shutdown
|
|
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
process.on(signal, async () => {
|
|
await cleanupTestBackend();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a non-admin user, add them to the workspace, and return their token.
|
|
*/
|
|
export async function createNonAdminUser(
|
|
backend: TestBackend,
|
|
workspaceId: string = backend.workspace
|
|
): Promise<string> {
|
|
if (!backend.apiRequest) {
|
|
throw new Error("Backend does not support apiRequest");
|
|
}
|
|
|
|
const email = `nonadmin_${Date.now()}@test.dev`;
|
|
const password = "testpass123";
|
|
|
|
// Create user globally (as admin)
|
|
const createResp = await backend.apiRequest("/api/users/create", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
email,
|
|
password,
|
|
super_admin: false,
|
|
name: "Non-Admin Test User",
|
|
}),
|
|
});
|
|
if (!createResp.ok) {
|
|
throw new Error(`Failed to create user: ${await createResp.text()}`);
|
|
}
|
|
await createResp.text();
|
|
|
|
// Add user to workspace as non-admin
|
|
const addResp = await backend.apiRequest(
|
|
`/api/w/${workspaceId}/workspaces/add_user`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
email,
|
|
is_admin: false,
|
|
operator: false,
|
|
}),
|
|
}
|
|
);
|
|
if (!addResp.ok) {
|
|
throw new Error(`Failed to add user to workspace: ${await addResp.text()}`);
|
|
}
|
|
await addResp.text();
|
|
|
|
// Login as the non-admin user to get a token
|
|
const loginResp = await fetch(`${backend.baseUrl}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
if (!loginResp.ok) {
|
|
throw new Error(`Failed to login as non-admin: ${await loginResp.text()}`);
|
|
}
|
|
return await loginResp.text();
|
|
}
|
|
|
|
/**
|
|
* Create workspace dependencies via the API (e.g. a shared package.json for bun scripts).
|
|
*/
|
|
export async function createRemoteWorkspaceDeps(
|
|
backend: TestBackend,
|
|
language: string,
|
|
content: string,
|
|
name?: string,
|
|
): Promise<void> {
|
|
if (!backend.apiRequest) {
|
|
throw new Error("Backend does not support apiRequest");
|
|
}
|
|
|
|
const resp = await backend.apiRequest(
|
|
`/api/w/${backend.workspace}/workspace_dependencies/create`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
workspace_id: backend.workspace,
|
|
language,
|
|
content,
|
|
...(name ? { name } : {}),
|
|
}),
|
|
}
|
|
);
|
|
if (!resp.ok) {
|
|
throw new Error(`Failed to create workspace deps (${resp.status}): ${await resp.text()}`);
|
|
}
|
|
await resp.text();
|
|
}
|
|
|
|
// Re-export for convenience
|
|
export type { CargoBackendConfig } from "./cargo_backend.ts";
|
|
export type { ContainerConfig } from "./containerized_backend.ts";
|