@akanjs/devkit 2.4.1-rc.2 → 2.4.1-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { DevStabilityHarness } from "./devStabilityHarness";
6
+
7
+ /**
8
+ * Port allocation and orphan sweeping, asserted directly rather than through the integration suite.
9
+ *
10
+ * Both used to be probabilistic: the offset was drawn at random, and the port was re-derived from the
11
+ * live `apps/` listing on every call. Neither failure shows up in a sequential run on a quiet machine —
12
+ * five consecutive runs of the integration file passed 16/16 with the old code — so the properties have
13
+ * to be asserted here, where a collision can be constructed instead of waited for.
14
+ */
15
+ const roots: string[] = [];
16
+ const servers: Bun.Server<undefined>[] = [];
17
+
18
+ const createRoot = async (): Promise<string> => {
19
+ const root = await mkdtemp(path.join(tmpdir(), "akan-harness-port-"));
20
+ roots.push(root);
21
+ await mkdir(path.join(root, "apps"), { recursive: true });
22
+ return root;
23
+ };
24
+
25
+ const occupy = (port: number): Bun.Server<undefined> => {
26
+ const server = Bun.serve({ port, fetch: () => new Response("occupied") });
27
+ servers.push(server);
28
+ return server;
29
+ };
30
+
31
+ afterEach(async () => {
32
+ for (const server of servers.splice(0)) server.stop(true);
33
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
34
+ });
35
+
36
+ describe("dev stability harness port allocation", () => {
37
+ test("hands every harness in a process a distinct port", async () => {
38
+ const workspaceRoot = await createRoot();
39
+ const ports: number[] = [];
40
+ // Sequentially, the way the suite creates them — one harness per test.
41
+ for (let i = 0; i < 12; i++) ports.push(await new DevStabilityHarness({ workspaceRoot }).resolvePort());
42
+
43
+ expect(new Set(ports).size).toBe(ports.length);
44
+ // Forward-only, so a harness cannot be handed a port a previous host in this process may still be
45
+ // holding while it shuts down — which is the collision the random offset kept producing.
46
+ expect(ports).toEqual([...ports].sort((a, b) => a - b));
47
+ });
48
+
49
+ test("returns the same port on every call instead of re-deriving it", async () => {
50
+ const workspaceRoot = await createRoot();
51
+ const harness = new DevStabilityHarness({ workspaceRoot });
52
+ const first = await harness.resolvePort();
53
+
54
+ // A rival fixture appearing shifts this app's index among locale-sorted apps, which is exactly what
55
+ // a parallel run does every few seconds. The answer must not move underneath a running test.
56
+ await mkdir(path.join(workspaceRoot, "apps", "aaa-rival"), { recursive: true });
57
+ await writeFile(path.join(workspaceRoot, "apps", "aaa-rival", "akan.config.ts"), "export default {};\n");
58
+
59
+ expect(await harness.resolvePort()).toBe(first);
60
+ });
61
+
62
+ test("skips a candidate port that something else already holds", async () => {
63
+ const workspaceRoot = await createRoot();
64
+ const taken =
65
+ (await new DevStabilityHarness({ workspaceRoot }).resolvePort()) + DevStabilityHarness.portOffsetStride;
66
+ occupy(taken);
67
+
68
+ const next = await new DevStabilityHarness({ workspaceRoot }).resolvePort();
69
+
70
+ // The next cursor step lands exactly on the occupied port, so a probe-less allocator would hand it
71
+ // out and the gateway would exit with "already in use" — it has no fallback for its http port.
72
+ expect(next).not.toBe(taken);
73
+ expect(await DevStabilityHarness.isPortFree(next)).toBe(true);
74
+ });
75
+
76
+ test("honours an explicit offset so a probe script can pin its port", async () => {
77
+ const workspaceRoot = await createRoot();
78
+ expect(await new DevStabilityHarness({ workspaceRoot, portOffset: 17 }).resolvePort()).toBe(8282 + 17);
79
+ });
80
+
81
+ test("reports a bound port as unavailable and a free one as available", async () => {
82
+ // Port 0 lets the OS pick a free one, so this cannot collide with anything on the machine.
83
+ const port = Number(occupy(0).port);
84
+ expect(await DevStabilityHarness.isPortFree(port)).toBe(false);
85
+ for (const server of servers.splice(0)) server.stop(true);
86
+ expect(await DevStabilityHarness.isPortFree(port)).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe("dev stability harness fixture sweep", () => {
91
+ test("removes fixtures whose owning test process is gone and keeps live ones", async () => {
92
+ const workspaceRoot = await createRoot();
93
+ // A pid that cannot be running: one below the 32-bit max, never assigned in practice.
94
+ const abandoned = `${DevStabilityHarness.fixturePrefix}2147483646-1700000000000`;
95
+ const live = `${DevStabilityHarness.fixturePrefix}${process.pid}-1700000000001`;
96
+ for (const name of [abandoned, live]) await mkdir(path.join(workspaceRoot, "apps", name), { recursive: true });
97
+
98
+ const swept = await DevStabilityHarness.sweepAbandonedFixtures(workspaceRoot);
99
+
100
+ expect(swept).toEqual([abandoned]);
101
+ // `readdir`, not `Bun.file(dir).exists()` — that reports false for a directory, so it would have
102
+ // asserted nothing here.
103
+ // Never swept by name alone: a concurrent run's fixtures look identical apart from the pid they
104
+ // carry, and taking one out from under a live suite would break the run this is meant to protect.
105
+ expect(await readdir(path.join(workspaceRoot, "apps"))).toEqual([live]);
106
+ });
107
+
108
+ test("leaves a workspace with no fixtures alone", async () => {
109
+ expect(await DevStabilityHarness.sweepAbandonedFixtures(await createRoot())).toEqual([]);
110
+ });
111
+ });