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
177 changes: 174 additions & 3 deletions packages/stack/src/managed-manager-lifecycle.integration.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
import { it } from "@effect/vitest";
import { NodeFileSystem, NodePath } from "@effect/platform-node";
import { Deferred, Effect, Fiber, FileSystem, Layer, ManagedRuntime, Schedule } from "effect";
import {
Cause,
Deferred,
Effect,
Fiber,
FileSystem,
Layer,
ManagedRuntime,
Schedule,
} from "effect";
import { HttpServer } from "effect/unstable/http";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { afterEach, describe, expect } from "vitest";
import { ManagedStackManager, managedStackManagerLayer } from "./managed/manager.ts";
import { gitConfigStoreLayer } from "./managed/git.ts";
import { acquireControl } from "./managed/control.ts";
import { acquireControl, ControlTransport } from "./managed/control.ts";
import { deriveStackId, ensureEnvironment } from "./managed/environment.ts";
import { controlTransportLayer } from "./platform-node.ts";
import { httpTransportClientLayer } from "./HttpTransportClient.ts";
Expand DownExpand Up@@ -304,6 +313,168 @@ describe("managed stack lifecycle journeys", () => {
);
});

it.live(
"keeps a stack document when its identity changes before delete ownership settles",
() => {
const { layer, workspace } = setup();
const copied = join(workspace, "..", "delete-race-copy");
let armed = false;
let markerSwapped!: Deferred.Deferred<void>;
const gatedTransport = Layer.effect(
ControlTransport,
Effect.gen(function* () {
const base = yield* ControlTransport;
return {
...base,
read: (endpoint: Parameters<typeof base.read>[0]) =>
Effect.gen(function* () {
if (armed) {
armed = false;
writeFileSync(
join(copied, ".supabase", "identity.json"),
readFileSync(join(workspace, ".supabase", "identity.json")),
);
yield* Deferred.succeed(markerSwapped, void 0);
}
return yield* base.read(endpoint);
}),
} satisfies typeof base;
}),
).pipe(Layer.provide(controlTransportLayer));
const managerLayer = layer.pipe(Layer.provide(gatedTransport));
return Effect.scoped(
Effect.gen(function* () {
markerSwapped = yield* Deferred.make<void>();
const manager = yield* ManagedStackManager;
const originalEnvironment = yield* ensureEnvironment(workspace);
const originalStackId = deriveStackId(originalEnvironment.identity, "default");
const originalOwner = yield* acquireControl({ stackId: originalStackId });
if (originalOwner._tag !== "Owned") throw new Error("expected original ownership");
const original = yield* manager.startStack({
workspacePath: workspace,
portDocument: automaticDocument(),
ownership: originalOwner,
lifecycle: "stopped",
});
yield* releaseLease(original);
yield* originalOwner.close;

mkdirSync(copied);
const copiedEnvironment = yield* ensureEnvironment(copied);
const copiedStackId = deriveStackId(copiedEnvironment.identity, "default");
const copiedOwner = yield* acquireControl({ stackId: copiedStackId });
if (copiedOwner._tag !== "Owned") throw new Error("expected copied ownership");
const copiedStack = yield* manager.startStack({
workspacePath: copied,
portDocument: automaticDocument(),
ownership: copiedOwner,
lifecycle: "stopped",
});
yield* releaseLease(copiedStack);

armed = true;
const deleting = yield* Effect.forkScoped(deleteManagedStack({ workspacePath: copied }));
yield* Deferred.await(markerSwapped);
yield* copiedOwner.close;
const result = yield* Fiber.join(deleting).pipe(Effect.exit);
expect(result._tag).toBe("Failure");
if (result._tag === "Failure") {
expect(Cause.squash(result.cause)).toMatchObject({
_tag: "InvalidManagedIdentityError",
});
}
expect(yield* manager.inspectStack(copiedStackId)).toBeDefined();
}),
).pipe(
Effect.provide(managerLayer),
Effect.provide(gatedTransport),
Effect.provide(NodeFileSystem.layer),
Effect.provide(NodePath.layer),
Effect.provide(gitConfigStoreLayer),
);
},
);

it.live("rejects stop when workspace identity changes while control ownership settles", () => {
const { layer, workspace } = setup();
const copied = join(workspace, "..", "stop-race-copy");
let armed = false;
let markerSwapped!: Deferred.Deferred<void>;
const gatedTransport = Layer.effect(
ControlTransport,
Effect.gen(function* () {
const base = yield* ControlTransport;
return {
...base,
read: (endpoint: Parameters<typeof base.read>[0]) =>
Effect.gen(function* () {
if (armed) {
armed = false;
writeFileSync(
join(copied, ".supabase", "identity.json"),
readFileSync(join(workspace, ".supabase", "identity.json")),
);
yield* Deferred.succeed(markerSwapped, void 0);
}
return yield* base.read(endpoint);
}),
} satisfies typeof base;
}),
).pipe(Layer.provide(controlTransportLayer));
const managerLayer = layer.pipe(Layer.provide(gatedTransport));
return Effect.scoped(
Effect.gen(function* () {
markerSwapped = yield* Deferred.make<void>();
const manager = yield* ManagedStackManager;
const originalEnvironment = yield* ensureEnvironment(workspace);
const originalStackId = deriveStackId(originalEnvironment.identity, "default");
const originalOwner = yield* acquireControl({ stackId: originalStackId });
if (originalOwner._tag !== "Owned") throw new Error("expected original ownership");
const original = yield* manager.startStack({
workspacePath: workspace,
portDocument: automaticDocument(),
ownership: originalOwner,
lifecycle: "stopped",
});
yield* releaseLease(original);
yield* originalOwner.close;

mkdirSync(copied);
const copiedEnvironment = yield* ensureEnvironment(copied);
const copiedStackId = deriveStackId(copiedEnvironment.identity, "default");
const copiedOwner = yield* acquireControl({ stackId: copiedStackId });
if (copiedOwner._tag !== "Owned") throw new Error("expected copied ownership");
const copiedStack = yield* manager.startStack({
workspacePath: copied,
portDocument: automaticDocument(),
ownership: copiedOwner,
lifecycle: "starting",
});
yield* releaseLease(copiedStack);

armed = true;
const stopping = yield* Effect.forkScoped(stopManagedStack({ workspacePath: copied }));
yield* Deferred.await(markerSwapped);
yield* copiedOwner.close;
const result = yield* Fiber.join(stopping).pipe(Effect.exit);
expect(result._tag).toBe("Failure");
if (result._tag === "Failure") {
expect(Cause.squash(result.cause)).toMatchObject({
_tag: "InvalidManagedIdentityError",
});
}
expect((yield* manager.inspectStack(copiedStackId))?.lifecycle).toBe("starting");
}),
).pipe(
Effect.provide(managerLayer),
Effect.provide(gatedTransport),
Effect.provide(NodeFileSystem.layer),
Effect.provide(NodePath.layer),
Effect.provide(gitConfigStoreLayer),
Effect.provide(httpTransportClientLayer),
);
});

it.live("deletes an owned stack when its document path is a directory", () => {
const { layer, stateRoot, workspace } = setup();
return Effect.scoped(
Expand Down
102 changes: 100 additions & 2 deletions packages/stack/src/managed-manager-projects.integration.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
import { it } from "@effect/vitest";
import { NodeFileSystem, NodePath } from "@effect/platform-node";
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect";
import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
import {
cpSync,
mkdirSync,
mkdtempSync,
realpathSync,
renameSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect } from "vitest";
import { ManagedStackManager } from "./managed/manager.ts";
import { gitConfigStoreLayer } from "./managed/git.ts";
import { acquireControl, ControlTransport } from "./managed/control.ts";
import { deriveStackId, ensureEnvironment } from "./managed/environment.ts";
import { controlTransportLayer } from "./platform-node.ts";
import { listStacks as listStackSummaries, resolveStackSummary } from "./discovery.ts";
import { makeRepository } from "../tests/helpers/git-workspace.ts";
import { git, makeRepository } from "../tests/helpers/git-workspace.ts";
import {
automaticDocument,
cleanupRoots,
Expand All@@ -23,6 +33,94 @@ afterEach(() => cleanupRoots(roots));
const setup = () => setupManagedManager(roots);

describe("managed stack projects journeys", () => {
it.live("rejects copied ordinary workspace identity until the original path is moved", () => {
const { layer, workspace } = setup();
return Effect.scoped(
Effect.gen(function* () {
const manager = yield* ManagedStackManager;
const environment = yield* ensureEnvironment(workspace);
const stackId = deriveStackId(environment.identity, "default");
const ownership = yield* acquireControl({ stackId });
if (ownership._tag !== "Owned") throw new Error("expected stack control ownership");
const initial = yield* manager.startStack({
workspacePath: workspace,
stackName: "default",
portDocument: automaticDocument(),
ownership,
lifecycle: "stopped",
});
yield* initial.lease.releaseAll;

const copied = join(workspace, "..", "workspace-copy");
cpSync(workspace, copied, { recursive: true });
const copiedDiscovery = yield* manager.discoverWorkspace(copied).pipe(Effect.exit);
expect(Exit.isFailure(copiedDiscovery)).toBe(true);
if (Exit.isFailure(copiedDiscovery)) {
const error = Cause.squash(copiedDiscovery.cause);
expect(error).toMatchObject({ _tag: "InvalidManagedIdentityError" });
if (error instanceof Error) {
expect(error.message).toContain("Delete");
expect(error.message).toContain(".supabase/identity.json");
}
}
const copiedEnsure = yield* manager.ensureWorkspace(copied).pipe(Effect.exit);
expect(Exit.isFailure(copiedEnsure)).toBe(true);
if (Exit.isFailure(copiedEnsure)) {
expect(Cause.squash(copiedEnsure.cause)).toMatchObject({
_tag: "InvalidManagedIdentityError",
});
}

const copiedStart = yield* manager
.startStack({
workspacePath: copied,
stackName: "default",
portDocument: automaticDocument(),
ownership,
lifecycle: "stopped",
})
.pipe(Effect.exit);
expect(Exit.isFailure(copiedStart)).toBe(true);
if (Exit.isFailure(copiedStart)) {
const error = Cause.squash(copiedStart.cause);
expect(error).toMatchObject({ _tag: "InvalidManagedIdentityError" });
if (error instanceof Error) {
expect(error.message).toContain("Delete");
expect(error.message).toContain(".supabase/identity.json");
}
}

git(workspace, "init", "-q", "-b", "main");
const copiedAfterOriginalGit = yield* manager.discoverWorkspace(copied);
expect(copiedAfterOriginalGit.state).toBe("ready");
rmSync(join(workspace, ".git"), { recursive: true, force: true });

rmSync(copied, { recursive: true, force: true });
renameSync(workspace, copied);
const movedDiscovery = yield* manager.discoverWorkspace(copied);
expect(movedDiscovery.state).toBe("ready");
writeFileSync(workspace, "stale workspace path");
const moved = yield* manager.startStack({
workspacePath: copied,
stackName: "default",
portDocument: automaticDocument(),
ownership,
lifecycle: "stopped",
});
expect(moved.stack.id).toBe(stackId);
expect(moved.stack.workspace.path).toBe(realpathSync(copied));
expect((yield* manager.inspectStack(stackId))?.workspace.path).toBe(realpathSync(copied));
yield* moved.lease.releaseAll;
}),
).pipe(
Effect.provide(layer),
Effect.provide(NodeFileSystem.layer),
Effect.provide(NodePath.layer),
Effect.provide(gitConfigStoreLayer),
Effect.provide(controlTransportLayer),
);
});

it.live("rejects empty and ASCII-control stack names before resolving a stack", () => {
const { layer, workspace } = setup();
return Effect.gen(function* () {
Expand Down
Loading
Loading