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
163 changes: 163 additions & 0 deletions packages/sandbox-sidecar/src/provisioner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import {
createAllocationStateStore,
type AllocationStateStore,
} from "./state-store";
import { createSidecarProvisioner } from "./provisioner";
import type { SidecarBackend } from "./backend";

const TOKEN = "s3cr3t-bootstrap-token";

let dataDir: string;
let store: AllocationStateStore;
let started: string[];

beforeEach(async () => {
dataDir = await mkdtemp(join(tmpdir(), "sandbox-sidecar-provisioner-"));
store = createAllocationStateStore(join(dataDir, "state.json"));
started = [];
});

afterEach(async () => {
await rm(dataDir, { recursive: true, force: true });
});

function fakeBackend(): SidecarBackend {
let counter = 0;
return {
async startUnit() {
counter += 1;
const externalRef = `unit-${String(counter)}`;
started.push(externalRef);
return externalRef;
},
async stopUnit() {},
async findUnitsByAllocation() {
return [];
},
};
}

function baseEnsureRequest(overrides: Partial<Record<string, unknown>> = {}) {
return {
allocationId: "alloc-1",
generation: 0,
tenantId: "tenant-1",
anchorRunId: "run-1",
sidecarId: "sidecar-1",
token: TOKEN,
hubWebSocketUrl: "wss://hub.example.com/ws",
...overrides,
} as never;
}

function makeProvisioner(backend: SidecarBackend = fakeBackend()) {
return createSidecarProvisioner({
id: "test-provisioner",
apiVersion: 1,
bindingFingerprint: "test",
capabilities: [],
backend,
store,
});
}

describe("ensure request validation", () => {
test("accepts generation 0, the allocation service's initial generation", async () => {
const provisioner = makeProvisioner();

const result = await provisioner.ensure(
baseEnsureRequest({ generation: 0 }),
);

expect(result).toEqual({ kind: "accepted", externalRef: "unit-1" });
});

test("accepts a later generation", async () => {
const provisioner = makeProvisioner();

await provisioner.ensure(baseEnsureRequest({ generation: 0 }));
const result = await provisioner.ensure(
baseEnsureRequest({ generation: 1 }),
);

expect(result).toEqual({ kind: "accepted", externalRef: "unit-2" });
});

test("rejects a negative generation", async () => {
const provisioner = makeProvisioner();

const result = await provisioner.ensure(
baseEnsureRequest({ generation: -1 }),
);

expect(result).toMatchObject({
kind: "rejected",
code: "invalid_ensure_request",
});
expect(started).toHaveLength(0);
});

test("rejects a non-integer generation", async () => {
const provisioner = makeProvisioner();

const result = await provisioner.ensure(
baseEnsureRequest({ generation: 1.5 }),
);

expect(result).toMatchObject({
kind: "rejected",
code: "invalid_ensure_request",
});
expect(started).toHaveLength(0);
});

test("still rejects a stale generation once fencing has observed a later one", async () => {
const provisioner = makeProvisioner();

await provisioner.ensure(baseEnsureRequest({ generation: 1 }));
const result = await provisioner.ensure(
baseEnsureRequest({ generation: 0 }),
);

expect(result).toMatchObject({
kind: "rejected",
code: "stale_generation",
});
expect(started).toHaveLength(1);
});
});

describe("destroy request validation", () => {
test("accepts generation 0", async () => {
const provisioner = makeProvisioner();
await provisioner.ensure(baseEnsureRequest({ generation: 0 }));

const result = await provisioner.destroy({
allocationId: "alloc-1",
sidecarId: "sidecar-1",
generation: 0,
});

expect(result).toEqual({ kind: "destroyed" });
});

test("rejects a negative generation", async () => {
const provisioner = makeProvisioner();

const result = await provisioner.destroy({
allocationId: "alloc-1",
sidecarId: "sidecar-1",
generation: -1,
});

expect(result).toMatchObject({
kind: "rejected",
code: "invalid_destroy_request",
});
});
});
8 changes: 4 additions & 4 deletions packages/sandbox-sidecar/src/provisioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,17 +218,17 @@ function validateEnsureRequest(request: EnsureSidecarRequest): string | null {
if (request.token === "") return "token must not be empty";
if (request.hubWebSocketUrl === "")
return "hubWebSocketUrl must not be empty";
if (!Number.isInteger(request.generation) || request.generation <= 0) {
return "generation must be a positive integer";
if (!Number.isInteger(request.generation) || request.generation < 0) {
return "generation must be a non-negative integer";
}
return null;
}

function validateDestroyRequest(request: DestroySidecarRequest): string | null {
if (request.allocationId === "") return "allocationId must not be empty";
if (request.sidecarId === "") return "sidecarId must not be empty";
if (!Number.isInteger(request.generation) || request.generation <= 0) {
return "generation must be a positive integer";
if (!Number.isInteger(request.generation) || request.generation < 0) {
return "generation must be a non-negative integer";
}
return null;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/sandbox-sidecar/src/state-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ describe("observeEnsure", () => {
expect(result.kind).toBe("observed");
});

test("accepts generation 0, the allocation service's initial generation", async () => {
const store = createAllocationStateStore(statePath());
const result = await store.observeEnsure({
allocationId: "alloc-1",
sidecarId: "sidecar-1",
generation: 0,
});
expect(result.kind).toBe("observed");
});

test("is idempotent when observed again at the same generation", async () => {
const store = createAllocationStateStore(statePath());
await store.observeEnsure({
Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox-sidecar/src/state-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { type } from "arktype";
const AllocationRecord = type({
allocationId: "string > 0",
sidecarId: "string > 0",
generation: "number.integer > 0",
generation: "number.integer >= 0",
desiredState: "'ensured' | 'destroyed'",
externalRef: "string | null",
tokenHashSha256: "string | null",
Expand Down
Loading