mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ddc8c846 |
@@ -12,6 +12,12 @@ describe("TaskCancel Hook", () => {
|
||||
let getEnv: () => { tempDir: string }
|
||||
let hookTestEnv: HookTestEnv
|
||||
|
||||
// On Windows, hooks execute via a PowerShell bridge that spawns a child
|
||||
// Node process. That double-process startup is slow and variable on CI and
|
||||
// can easily exceed Mocha's default 2 s timeout, so spawning tests opt into a
|
||||
// larger timeout. Mirrors taskresume/hook-factory/user-prompt-submit tests.
|
||||
const WINDOWS_HOOK_TEST_TIMEOUT_MS = 15000
|
||||
|
||||
const writeHookScript = async (hookPath: string, nodeScript: string): Promise<void> => {
|
||||
await writeHookScriptForPlatform(hookPath, nodeScript)
|
||||
}
|
||||
@@ -29,7 +35,11 @@ describe("TaskCancel Hook", () => {
|
||||
})
|
||||
|
||||
describe("Hook Input Format", () => {
|
||||
it("should receive task metadata with completionStatus", async () => {
|
||||
it("should receive task metadata with completionStatus", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -61,7 +71,11 @@ console.log(JSON.stringify({
|
||||
// Note: contextModification is ignored for TaskCancel hooks
|
||||
})
|
||||
|
||||
it("should handle 'abandoned' completion status", async () => {
|
||||
it("should handle 'abandoned' completion status", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -96,7 +110,11 @@ console.log(JSON.stringify({
|
||||
// Note: contextModification is ignored for TaskCancel hooks
|
||||
})
|
||||
|
||||
it("should receive all common hook input fields", async () => {
|
||||
it("should receive all common hook input fields", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -136,7 +154,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Fire-and-Forget Behavior", () => {
|
||||
it("should ignore contextModification regardless of content", async () => {
|
||||
it("should ignore contextModification regardless of content", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.log(JSON.stringify({
|
||||
@@ -192,7 +214,11 @@ console.log(JSON.stringify({
|
||||
// The contextModification value is different but behavior is identical (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should succeed regardless of hook return value", async () => {
|
||||
it("should succeed regardless of hook return value", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
// Note: contextModification is ignored for TaskCancel hooks
|
||||
@@ -222,7 +248,11 @@ console.log(JSON.stringify({
|
||||
result.cancel.should.be.false()
|
||||
})
|
||||
|
||||
it("should return error message when hook returns cancel: true", async () => {
|
||||
it("should return error message when hook returns cancel: true", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.log(JSON.stringify({
|
||||
@@ -254,7 +284,11 @@ console.log(JSON.stringify({
|
||||
result.errorMessage?.should.equal("Hook tried to block cancellation")
|
||||
})
|
||||
|
||||
it("should execute without errors for cleanup purposes", async () => {
|
||||
it("should execute without errors for cleanup purposes", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -288,7 +322,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should surface hook errors to the user", async () => {
|
||||
it("should surface hook errors to the user", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.error("Hook execution error");
|
||||
@@ -317,7 +355,11 @@ process.exit(1);`
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle malformed JSON output from hook", async () => {
|
||||
it("should handle malformed JSON output from hook", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const hookPath = path.join(tempDir, ".clinerules", "hooks", "TaskCancel")
|
||||
const hookScript = `#!/usr/bin/env node
|
||||
console.log("not valid json")`
|
||||
@@ -359,7 +401,11 @@ console.log("not valid json")`
|
||||
stubHookDirs(sandbox, [globalHooksDir, workspaceHooksDir])
|
||||
})
|
||||
|
||||
it("should execute both global and workspace TaskCancel hooks", async () => {
|
||||
it("should execute both global and workspace TaskCancel hooks", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
// Create global hook
|
||||
const globalHookPath = path.join(globalHooksDir, "TaskCancel")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
@@ -399,7 +445,11 @@ console.log(JSON.stringify({
|
||||
// Both hooks executed successfully
|
||||
})
|
||||
|
||||
it("should execute both hooks with different completion statuses", async () => {
|
||||
it("should execute both hooks with different completion statuses", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const globalHookPath = path.join(globalHooksDir, "TaskCancel")
|
||||
const globalHookScript = `#!/usr/bin/env node
|
||||
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
|
||||
@@ -441,7 +491,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("No Hook Behavior", () => {
|
||||
it("should succeed when no hook exists", async () => {
|
||||
it("should succeed when no hook exists", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
const factory = new HookFactory()
|
||||
const runner = await factory.create("TaskCancel")
|
||||
|
||||
@@ -461,7 +515,11 @@ console.log(JSON.stringify({
|
||||
})
|
||||
|
||||
describe("Fixture-Based Tests", () => {
|
||||
it("should handle cancel: true with no error message", async () => {
|
||||
it("should handle cancel: true with no error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/false-no-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -484,7 +542,11 @@ console.log(JSON.stringify({
|
||||
// Cancellation still proceeds (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should handle cancel: true with error message", async () => {
|
||||
it("should handle cancel: true with error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/false-with-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -507,7 +569,11 @@ console.log(JSON.stringify({
|
||||
// Cancellation still proceeds (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should handle cancel: false with no error message", async () => {
|
||||
it("should handle cancel: false with no error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/true-no-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -529,7 +595,11 @@ console.log(JSON.stringify({
|
||||
// Normal success case - no errors to surface
|
||||
})
|
||||
|
||||
it("should handle cancel: false with error message", async () => {
|
||||
it("should handle cancel: false with error message", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/true-with-error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
@@ -553,7 +623,11 @@ console.log(JSON.stringify({
|
||||
// Cancellation still proceeds (fire-and-forget)
|
||||
})
|
||||
|
||||
it("should handle hook that exits with non-zero status code", async () => {
|
||||
it("should handle hook that exits with non-zero status code", async function () {
|
||||
if (process.platform === "win32") {
|
||||
this.timeout(WINDOWS_HOOK_TEST_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
await loadFixture("hooks/taskcancel/error", getEnv().tempDir)
|
||||
|
||||
const factory = new HookFactory()
|
||||
|
||||
@@ -16,6 +16,12 @@ const sqliteAvailable = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
// The first SQLite-backed test in a file pays a one-time cost: loading the
|
||||
// native `node:sqlite` module and creating the first temp database file. On
|
||||
// Windows CI this cold start can exceed Vitest's default 5 s timeout, so give
|
||||
// these tests extra headroom.
|
||||
const SQLITE_TEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function createTempDbPath(): Promise<string> {
|
||||
const directory = await mkdtemp(join(tmpdir(), "sdk-hub-schedule-"));
|
||||
return join(directory, "cron.db");
|
||||
@@ -34,129 +40,137 @@ afterEach(async () => {
|
||||
describe("HubScheduleService", () => {
|
||||
const sqliteIt = sqliteAvailable ? it : it.skip;
|
||||
|
||||
sqliteIt("creates, triggers, and reports schedule history", async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-1" })),
|
||||
sendSession: vi.fn(async () => ({
|
||||
result: {
|
||||
text: "done",
|
||||
iterations: 3,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
usage: { totalCost: 1.25 },
|
||||
sqliteIt(
|
||||
"creates, triggers, and reports schedule history",
|
||||
async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-1" })),
|
||||
sendSession: vi.fn(async () => ({
|
||||
result: {
|
||||
text: "done",
|
||||
iterations: 3,
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
usage: { totalCost: 1.25 },
|
||||
},
|
||||
})),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run the routine",
|
||||
workspaceRoot: "/workspace",
|
||||
cwd: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
},
|
||||
})),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run the routine",
|
||||
workspaceRoot: "/workspace",
|
||||
cwd: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
},
|
||||
maxParallel: 1,
|
||||
timeoutSeconds: 30,
|
||||
metadata: { delivery: { threadId: "thread-1" } },
|
||||
});
|
||||
maxParallel: 1,
|
||||
timeoutSeconds: 30,
|
||||
metadata: { delivery: { threadId: "thread-1" } },
|
||||
});
|
||||
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("success");
|
||||
expect(execution?.sessionId).toBe("session-1");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.completed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-1",
|
||||
status: "success",
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("success");
|
||||
expect(execution?.sessionId).toBe("session-1");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.completed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-1",
|
||||
status: "success",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const schedule = service.getSchedule(created.scheduleId);
|
||||
expect(schedule?.metadata).toEqual({
|
||||
delivery: { threadId: "thread-1" },
|
||||
});
|
||||
expect(
|
||||
service.listScheduleExecutions({ scheduleId: created.scheduleId }),
|
||||
).toHaveLength(1);
|
||||
expect(service.getScheduleStats(created.scheduleId).totalRuns).toBe(1);
|
||||
expect(service.getUpcomingRuns(10)).toHaveLength(1);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
SQLITE_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
sqliteIt(
|
||||
"publishes failed schedule execution events",
|
||||
async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-failed" })),
|
||||
sendSession: vi.fn(async () => {
|
||||
throw new Error("runtime failed");
|
||||
}),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
]);
|
||||
|
||||
const schedule = service.getSchedule(created.scheduleId);
|
||||
expect(schedule?.metadata).toEqual({
|
||||
delivery: { threadId: "thread-1" },
|
||||
});
|
||||
expect(
|
||||
service.listScheduleExecutions({ scheduleId: created.scheduleId }),
|
||||
).toHaveLength(1);
|
||||
expect(service.getScheduleStats(created.scheduleId).totalRuns).toBe(1);
|
||||
expect(service.getUpcomingRuns(10)).toHaveLength(1);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
sqliteIt("publishes failed schedule execution events", async () => {
|
||||
const dbPath = await createTempDbPath();
|
||||
cleanupPaths.push(dbPath);
|
||||
const publishedEvents: Array<{
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}> = [];
|
||||
const service = new HubScheduleService({
|
||||
dbPath,
|
||||
runtimeHandlers: {
|
||||
startSession: vi.fn(async () => ({ sessionId: "session-failed" })),
|
||||
sendSession: vi.fn(async () => {
|
||||
throw new Error("runtime failed");
|
||||
}),
|
||||
abortSession: vi.fn(async () => ({ applied: true })),
|
||||
stopSession: vi.fn(async () => ({ applied: true })),
|
||||
},
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Failure routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run and fail",
|
||||
workspaceRoot: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
eventPublisher: (eventType, payload) => {
|
||||
publishedEvents.push({ eventType, payload });
|
||||
},
|
||||
});
|
||||
try {
|
||||
const created = service.createSchedule({
|
||||
name: "Failure routine",
|
||||
cronPattern: "0 * * * *",
|
||||
prompt: "Run and fail",
|
||||
workspaceRoot: "/workspace",
|
||||
modelSelection: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-5.3-codex",
|
||||
},
|
||||
});
|
||||
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("failed");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.failed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-failed",
|
||||
status: "failed",
|
||||
errorMessage: "runtime failed",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
});
|
||||
const execution = await service.triggerScheduleNow(created.scheduleId);
|
||||
expect(execution?.status).toBe("failed");
|
||||
expect(publishedEvents).toEqual([
|
||||
{
|
||||
eventType: "schedule.execution.failed",
|
||||
payload: expect.objectContaining({
|
||||
scheduleId: created.scheduleId,
|
||||
executionId: execution?.executionId,
|
||||
sessionId: "session-failed",
|
||||
status: "failed",
|
||||
errorMessage: "runtime failed",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
SQLITE_TEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
sqliteIt(
|
||||
"handles schedule commands through the hub command adapter",
|
||||
@@ -209,5 +223,6 @@ describe("HubScheduleService", () => {
|
||||
await service.dispose();
|
||||
}
|
||||
},
|
||||
SQLITE_TEST_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,12 @@ import {
|
||||
startHubWebSocketServer,
|
||||
} from "../server";
|
||||
|
||||
// The first test that boots the hub server pays a one-time cold-start cost
|
||||
// (loading the native `node:sqlite` module used by the schedule runtime plus the
|
||||
// first server bind and discovery-file write). On Windows CI this can exceed
|
||||
// Vitest's default 5 s timeout, so give the startup test extra headroom.
|
||||
const HUB_SERVER_COLD_START_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function reservePort(): Promise<number> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const server = createNetServer();
|
||||
@@ -84,37 +90,43 @@ describe("hub server startup", () => {
|
||||
servers.clear();
|
||||
});
|
||||
|
||||
it("starts on the requested port instead of drifting to a random port", async () => {
|
||||
const owner = createInMemoryHubOwnerContext("hub-server-test-fixed-port");
|
||||
const port = await reservePort();
|
||||
await writeHubDiscovery(owner.discoveryPath, {
|
||||
hubId: "stale-hub",
|
||||
protocolVersion: "v1",
|
||||
authToken: "stale-token",
|
||||
host: "127.0.0.1",
|
||||
port: port + 1,
|
||||
url: `ws://127.0.0.1:${port + 1}/hub`,
|
||||
startedAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
});
|
||||
it(
|
||||
"starts on the requested port instead of drifting to a random port",
|
||||
async () => {
|
||||
const owner = createInMemoryHubOwnerContext("hub-server-test-fixed-port");
|
||||
const port = await reservePort();
|
||||
await writeHubDiscovery(owner.discoveryPath, {
|
||||
hubId: "stale-hub",
|
||||
protocolVersion: "v1",
|
||||
authToken: "stale-token",
|
||||
host: "127.0.0.1",
|
||||
port: port + 1,
|
||||
url: `ws://127.0.0.1:${port + 1}/hub`,
|
||||
startedAt: new Date(0).toISOString(),
|
||||
updatedAt: new Date(0).toISOString(),
|
||||
});
|
||||
|
||||
const result = await ensureHubWebSocketServer({
|
||||
owner,
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
pathname: "/hub",
|
||||
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
||||
});
|
||||
expect(result.url).toBe(`ws://127.0.0.1:${port}/hub`);
|
||||
expect(result.action).toBe("started");
|
||||
const server = requireServer(result.server);
|
||||
servers.add(server);
|
||||
const result = await ensureHubWebSocketServer({
|
||||
owner,
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
pathname: "/hub",
|
||||
runtimeHandlers: createLocalHubScheduleRuntimeHandlers(),
|
||||
});
|
||||
expect(result.url).toBe(`ws://127.0.0.1:${port}/hub`);
|
||||
expect(result.action).toBe("started");
|
||||
const server = requireServer(result.server);
|
||||
servers.add(server);
|
||||
|
||||
await expect(readHubDiscovery(owner.discoveryPath)).resolves.toMatchObject({
|
||||
port,
|
||||
url: `ws://127.0.0.1:${port}/hub`,
|
||||
});
|
||||
});
|
||||
await expect(
|
||||
readHubDiscovery(owner.discoveryPath),
|
||||
).resolves.toMatchObject({
|
||||
port,
|
||||
url: `ws://127.0.0.1:${port}/hub`,
|
||||
});
|
||||
},
|
||||
HUB_SERVER_COLD_START_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it("fails when the requested port is already occupied", async () => {
|
||||
const owner = createInMemoryHubOwnerContext("hub-server-test-port-busy");
|
||||
|
||||
Reference in New Issue
Block a user