style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,11 +1,19 @@
import { describe, test, expect, beforeEach, afterAll, mock, spyOn } from "bun:test";
import {
describe,
test,
expect,
beforeEach,
afterAll,
mock,
spyOn,
} from 'bun:test'
// Mock config before importing modules that depend on it
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-key-1", "test-key-2"],
baseUrl: "",
host: '0.0.0.0',
apiKeys: ['test-key-1', 'test-key-2'],
baseUrl: '',
pollTimeout: 8,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
@@ -13,153 +21,157 @@ const mockConfig = {
webCorsOrigins: [],
wsIdleTimeout: 30,
wsKeepaliveInterval: 20,
};
}
mock.module("../config", () => ({
mock.module('../config', () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
getBaseUrl: () => 'http://localhost:3000',
}))
import { validateApiKey, hashApiKey } from "../auth/api-key";
import { generateWorkerJwt, verifyWorkerJwt } from "../auth/jwt";
import { issueToken, resolveToken } from "../auth/token";
import { storeReset, storeCreateUser } from "../store";
import { validateApiKey, hashApiKey } from '../auth/api-key'
import { generateWorkerJwt, verifyWorkerJwt } from '../auth/jwt'
import { issueToken, resolveToken } from '../auth/token'
import { storeReset, storeCreateUser } from '../store'
// ---------- api-key ----------
describe("validateApiKey", () => {
test("validates a configured API key", () => {
expect(validateApiKey("test-key-1")).toBe(true);
expect(validateApiKey("test-key-2")).toBe(true);
});
describe('validateApiKey', () => {
test('validates a configured API key', () => {
expect(validateApiKey('test-key-1')).toBe(true)
expect(validateApiKey('test-key-2')).toBe(true)
})
test("rejects unknown key", () => {
expect(validateApiKey("unknown-key")).toBe(false);
});
test('rejects unknown key', () => {
expect(validateApiKey('unknown-key')).toBe(false)
})
test("rejects undefined", () => {
expect(validateApiKey(undefined)).toBe(false);
});
test('rejects undefined', () => {
expect(validateApiKey(undefined)).toBe(false)
})
test("rejects empty string", () => {
expect(validateApiKey("")).toBe(false);
});
});
test('rejects empty string', () => {
expect(validateApiKey('')).toBe(false)
})
})
describe("hashApiKey", () => {
test("produces consistent SHA-256 hex", () => {
const hash = hashApiKey("my-key");
expect(hash).toMatch(/^[0-9a-f]{64}$/);
expect(hashApiKey("my-key")).toBe(hash);
});
describe('hashApiKey', () => {
test('produces consistent SHA-256 hex', () => {
const hash = hashApiKey('my-key')
expect(hash).toMatch(/^[0-9a-f]{64}$/)
expect(hashApiKey('my-key')).toBe(hash)
})
test("different keys produce different hashes", () => {
expect(hashApiKey("key-a")).not.toBe(hashApiKey("key-b"));
});
});
test('different keys produce different hashes', () => {
expect(hashApiKey('key-a')).not.toBe(hashApiKey('key-b'))
})
})
// ---------- jwt ----------
describe("JWT", () => {
describe('JWT', () => {
// JWT reads process.env.RCS_API_KEYS directly (not via config)
const originalKeys = process.env.RCS_API_KEYS;
const originalKeys = process.env.RCS_API_KEYS
beforeEach(() => {
process.env.RCS_API_KEYS = "jwt-test-secret";
});
process.env.RCS_API_KEYS = 'jwt-test-secret'
})
afterAll(() => {
process.env.RCS_API_KEYS = originalKeys;
});
process.env.RCS_API_KEYS = originalKeys
})
describe("generateWorkerJwt", () => {
test("produces a three-part base64url token", () => {
const token = generateWorkerJwt("ses_123", 3600);
const parts = token.split(".");
expect(parts).toHaveLength(3);
describe('generateWorkerJwt', () => {
test('produces a three-part base64url token', () => {
const token = generateWorkerJwt('ses_123', 3600)
const parts = token.split('.')
expect(parts).toHaveLength(3)
for (const part of parts) {
expect(part).toMatch(/^[A-Za-z0-9_-]+$/);
expect(part).toMatch(/^[A-Za-z0-9_-]+$/)
}
});
})
test("contains correct header", () => {
const token = generateWorkerJwt("ses_123", 3600);
const header = JSON.parse(atob(token.split(".")[0].replace(/-/g, "+").replace(/_/g, "/")));
expect(header.alg).toBe("HS256");
expect(header.typ).toBe("JWT");
});
test('contains correct header', () => {
const token = generateWorkerJwt('ses_123', 3600)
const header = JSON.parse(
atob(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')),
)
expect(header.alg).toBe('HS256')
expect(header.typ).toBe('JWT')
})
test("throws when no API key configured", () => {
delete process.env.RCS_API_KEYS;
expect(() => generateWorkerJwt("ses_123", 3600)).toThrow("No API key configured");
process.env.RCS_API_KEYS = "jwt-test-secret";
});
});
test('throws when no API key configured', () => {
delete process.env.RCS_API_KEYS
expect(() => generateWorkerJwt('ses_123', 3600)).toThrow(
'No API key configured',
)
process.env.RCS_API_KEYS = 'jwt-test-secret'
})
})
describe("verifyWorkerJwt", () => {
test("verifies a valid token", () => {
const token = generateWorkerJwt("ses_abc", 3600);
const payload = verifyWorkerJwt(token);
expect(payload).not.toBeNull();
expect(payload!.session_id).toBe("ses_abc");
expect(payload!.role).toBe("worker");
expect(payload!.iat).toBeGreaterThan(0);
expect(payload!.exp).toBeGreaterThan(payload!.iat);
});
describe('verifyWorkerJwt', () => {
test('verifies a valid token', () => {
const token = generateWorkerJwt('ses_abc', 3600)
const payload = verifyWorkerJwt(token)
expect(payload).not.toBeNull()
expect(payload!.session_id).toBe('ses_abc')
expect(payload!.role).toBe('worker')
expect(payload!.iat).toBeGreaterThan(0)
expect(payload!.exp).toBeGreaterThan(payload!.iat)
})
test("returns null for expired token", () => {
const token = generateWorkerJwt("ses_old", -10);
expect(verifyWorkerJwt(token)).toBeNull();
});
test('returns null for expired token', () => {
const token = generateWorkerJwt('ses_old', -10)
expect(verifyWorkerJwt(token)).toBeNull()
})
test("returns null for malformed token (not 3 parts)", () => {
expect(verifyWorkerJwt("a.b")).toBeNull();
expect(verifyWorkerJwt("just-a-string")).toBeNull();
});
test('returns null for malformed token (not 3 parts)', () => {
expect(verifyWorkerJwt('a.b')).toBeNull()
expect(verifyWorkerJwt('just-a-string')).toBeNull()
})
test("returns null for tampered signature", () => {
const token = generateWorkerJwt("ses_123", 3600);
const parts = token.split(".");
const tampered = `${parts[0]}.${parts[1]}.${parts[2].slice(0, -4)}xxxx`;
expect(verifyWorkerJwt(tampered)).toBeNull();
});
test('returns null for tampered signature', () => {
const token = generateWorkerJwt('ses_123', 3600)
const parts = token.split('.')
const tampered = `${parts[0]}.${parts[1]}.${parts[2].slice(0, -4)}xxxx`
expect(verifyWorkerJwt(tampered)).toBeNull()
})
test("returns null for wrong signing key", () => {
const token = generateWorkerJwt("ses_123", 3600);
process.env.RCS_API_KEYS = "wrong-key";
expect(verifyWorkerJwt(token)).toBeNull();
process.env.RCS_API_KEYS = "jwt-test-secret";
});
});
});
test('returns null for wrong signing key', () => {
const token = generateWorkerJwt('ses_123', 3600)
process.env.RCS_API_KEYS = 'wrong-key'
expect(verifyWorkerJwt(token)).toBeNull()
process.env.RCS_API_KEYS = 'jwt-test-secret'
})
})
})
// ---------- token ----------
describe("issueToken / resolveToken", () => {
describe('issueToken / resolveToken', () => {
beforeEach(() => {
storeReset();
});
storeReset()
})
test("issues and resolves a token", () => {
storeCreateUser("alice");
const { token, expires_in } = issueToken("alice");
expect(token).toMatch(/^rct_\d+_[0-9a-f]+$/);
expect(expires_in).toBe(86400);
expect(resolveToken(token)).toBe("alice");
});
test('issues and resolves a token', () => {
storeCreateUser('alice')
const { token, expires_in } = issueToken('alice')
expect(token).toMatch(/^rct_\d+_[0-9a-f]+$/)
expect(expires_in).toBe(86400)
expect(resolveToken(token)).toBe('alice')
})
test("returns null for unknown token", () => {
expect(resolveToken("nonexistent")).toBeNull();
});
test('returns null for unknown token', () => {
expect(resolveToken('nonexistent')).toBeNull()
})
test("returns null for undefined token", () => {
expect(resolveToken(undefined)).toBeNull();
});
test('returns null for undefined token', () => {
expect(resolveToken(undefined)).toBeNull()
})
test("tokens are unique", () => {
storeCreateUser("alice");
const t1 = issueToken("alice").token;
const t2 = issueToken("alice").token;
expect(t1).not.toBe(t2);
});
});
test('tokens are unique', () => {
storeCreateUser('alice')
const t1 = issueToken('alice').token
const t2 = issueToken('alice').token
expect(t1).not.toBe(t2)
})
})

View File

@@ -1,182 +1,225 @@
import { describe, test, expect } from "bun:test";
import { describe, test, expect } from 'bun:test'
import {
getAutomationStateSnapshot,
getAutomationStateEventPayload,
automationStatesEqual,
} from "../services/automationState";
import type { AutomationStateResponse } from "../types/api";
} from '../services/automationState'
import type { AutomationStateResponse } from '../types/api'
// =============================================================================
// normalizeAutomationState (via getAutomationStateSnapshot)
// =============================================================================
describe("normalizeAutomationState", () => {
test("returns undefined when metadata has no automation_state key", () => {
expect(getAutomationStateSnapshot({})).toBeUndefined();
expect(getAutomationStateSnapshot({ other: true })).toBeUndefined();
expect(getAutomationStateSnapshot(null)).toBeUndefined();
expect(getAutomationStateSnapshot(undefined)).toBeUndefined();
});
describe('normalizeAutomationState', () => {
test('returns undefined when metadata has no automation_state key', () => {
expect(getAutomationStateSnapshot({})).toBeUndefined()
expect(getAutomationStateSnapshot({ other: true })).toBeUndefined()
expect(getAutomationStateSnapshot(null)).toBeUndefined()
expect(getAutomationStateSnapshot(undefined)).toBeUndefined()
})
test("returns disabled state for null automation_state", () => {
const result = getAutomationStateSnapshot({ automation_state: null });
test('returns disabled state for null automation_state', () => {
const result = getAutomationStateSnapshot({ automation_state: null })
expect(result).toEqual({
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
});
});
})
})
test("returns disabled state for non-object automation_state", () => {
for (const val of ["string", 123, true, []]) {
const result = getAutomationStateSnapshot({ automation_state: val });
expect(result?.enabled).toBe(false);
test('returns disabled state for non-object automation_state', () => {
for (const val of ['string', 123, true, []]) {
const result = getAutomationStateSnapshot({ automation_state: val })
expect(result?.enabled).toBe(false)
}
});
})
test("normalizes enabled: true correctly", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true } });
expect(result?.enabled).toBe(true);
});
test("normalizes enabled to false for non-true values", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: "yes" } });
expect(result?.enabled).toBe(false);
});
test("accepts phase: standby", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, phase: "standby" } });
expect(result?.phase).toBe("standby");
});
test("accepts phase: sleeping", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, phase: "sleeping" } });
expect(result?.phase).toBe("sleeping");
});
test("rejects invalid phase values", () => {
for (const phase of ["running", "idle", "active", "", null]) {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, phase } });
expect(result?.phase).toBeNull();
}
});
test("normalizes next_tick_at as number", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, next_tick_at: 12345 } });
expect(result?.next_tick_at).toBe(12345);
});
test("normalizes next_tick_at as null for non-number", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, next_tick_at: "soon" } });
expect(result?.next_tick_at).toBeNull();
});
test("normalizes sleep_until as number", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, sleep_until: 99999 } });
expect(result?.sleep_until).toBe(99999);
});
test("normalizes sleep_until as null for non-number", () => {
const result = getAutomationStateSnapshot({ automation_state: { enabled: true, sleep_until: false } });
expect(result?.sleep_until).toBeNull();
});
test("fully normalizes a complete valid state", () => {
test('normalizes enabled: true correctly', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, phase: "sleeping", next_tick_at: 100, sleep_until: 200 },
});
automation_state: { enabled: true },
})
expect(result?.enabled).toBe(true)
})
test('normalizes enabled to false for non-true values', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: 'yes' },
})
expect(result?.enabled).toBe(false)
})
test('accepts phase: standby', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, phase: 'standby' },
})
expect(result?.phase).toBe('standby')
})
test('accepts phase: sleeping', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, phase: 'sleeping' },
})
expect(result?.phase).toBe('sleeping')
})
test('rejects invalid phase values', () => {
for (const phase of ['running', 'idle', 'active', '', null]) {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, phase },
})
expect(result?.phase).toBeNull()
}
})
test('normalizes next_tick_at as number', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, next_tick_at: 12345 },
})
expect(result?.next_tick_at).toBe(12345)
})
test('normalizes next_tick_at as null for non-number', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, next_tick_at: 'soon' },
})
expect(result?.next_tick_at).toBeNull()
})
test('normalizes sleep_until as number', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, sleep_until: 99999 },
})
expect(result?.sleep_until).toBe(99999)
})
test('normalizes sleep_until as null for non-number', () => {
const result = getAutomationStateSnapshot({
automation_state: { enabled: true, sleep_until: false },
})
expect(result?.sleep_until).toBeNull()
})
test('fully normalizes a complete valid state', () => {
const result = getAutomationStateSnapshot({
automation_state: {
enabled: true,
phase: 'sleeping',
next_tick_at: 100,
sleep_until: 200,
},
})
expect(result).toEqual({
enabled: true,
phase: "sleeping",
phase: 'sleeping',
next_tick_at: 100,
sleep_until: 200,
});
});
});
})
})
})
// =============================================================================
// getAutomationStateEventPayload
// =============================================================================
describe("getAutomationStateEventPayload", () => {
test("returns disabled default when no automation_state in metadata", () => {
const result = getAutomationStateEventPayload({});
describe('getAutomationStateEventPayload', () => {
test('returns disabled default when no automation_state in metadata', () => {
const result = getAutomationStateEventPayload({})
expect(result).toEqual({
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
});
});
})
})
test("returns disabled default for null metadata", () => {
const result = getAutomationStateEventPayload(null);
test('returns disabled default for null metadata', () => {
const result = getAutomationStateEventPayload(null)
expect(result).toEqual({
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
});
});
})
})
test("returns normalized state when automation_state present", () => {
test('returns normalized state when automation_state present', () => {
const result = getAutomationStateEventPayload({
automation_state: { enabled: true, phase: "standby", next_tick_at: 50, sleep_until: 60 },
});
automation_state: {
enabled: true,
phase: 'standby',
next_tick_at: 50,
sleep_until: 60,
},
})
expect(result).toEqual({
enabled: true,
phase: "standby",
phase: 'standby',
next_tick_at: 50,
sleep_until: 60,
});
});
})
})
test("returns a new object each call (not frozen reference)", () => {
const a = getAutomationStateEventPayload({});
const b = getAutomationStateEventPayload({});
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
});
test('returns a new object each call (not frozen reference)', () => {
const a = getAutomationStateEventPayload({})
const b = getAutomationStateEventPayload({})
expect(a).toEqual(b)
expect(a).not.toBe(b)
})
})
// =============================================================================
// automationStatesEqual
// =============================================================================
describe("automationStatesEqual", () => {
describe('automationStatesEqual', () => {
const base: AutomationStateResponse = {
enabled: true,
phase: "standby",
phase: 'standby',
next_tick_at: 100,
sleep_until: 200,
};
}
test("returns true for identical states", () => {
expect(automationStatesEqual(base, { ...base })).toBe(true);
});
test('returns true for identical states', () => {
expect(automationStatesEqual(base, { ...base })).toBe(true)
})
test("returns false when enabled differs", () => {
expect(automationStatesEqual(base, { ...base, enabled: false })).toBe(false);
});
test('returns false when enabled differs', () => {
expect(automationStatesEqual(base, { ...base, enabled: false })).toBe(false)
})
test("returns false when phase differs", () => {
expect(automationStatesEqual(base, { ...base, phase: "sleeping" })).toBe(false);
expect(automationStatesEqual(base, { ...base, phase: null })).toBe(false);
});
test('returns false when phase differs', () => {
expect(automationStatesEqual(base, { ...base, phase: 'sleeping' })).toBe(
false,
)
expect(automationStatesEqual(base, { ...base, phase: null })).toBe(false)
})
test("returns false when next_tick_at differs", () => {
expect(automationStatesEqual(base, { ...base, next_tick_at: 999 })).toBe(false);
expect(automationStatesEqual(base, { ...base, next_tick_at: null })).toBe(false);
});
test('returns false when next_tick_at differs', () => {
expect(automationStatesEqual(base, { ...base, next_tick_at: 999 })).toBe(
false,
)
expect(automationStatesEqual(base, { ...base, next_tick_at: null })).toBe(
false,
)
})
test("returns false when sleep_until differs", () => {
expect(automationStatesEqual(base, { ...base, sleep_until: 999 })).toBe(false);
expect(automationStatesEqual(base, { ...base, sleep_until: null })).toBe(false);
});
test('returns false when sleep_until differs', () => {
expect(automationStatesEqual(base, { ...base, sleep_until: 999 })).toBe(
false,
)
expect(automationStatesEqual(base, { ...base, sleep_until: null })).toBe(
false,
)
})
test("returns true when both are disabled defaults", () => {
const disabled: AutomationStateResponse = { enabled: false, phase: null, next_tick_at: null, sleep_until: null };
expect(automationStatesEqual(disabled, { ...disabled })).toBe(true);
});
});
test('returns true when both are disabled defaults', () => {
const disabled: AutomationStateResponse = {
enabled: false,
phase: null,
next_tick_at: null,
sleep_until: null,
}
expect(automationStatesEqual(disabled, { ...disabled })).toBe(true)
})
})

View File

@@ -1,256 +1,276 @@
import { describe, test, expect } from "bun:test";
import { toClientPayload } from "../transport/client-payload";
import type { SessionEvent } from "../transport/event-bus";
import { describe, test, expect } from 'bun:test'
import { toClientPayload } from '../transport/client-payload'
import type { SessionEvent } from '../transport/event-bus'
function makeEvent(overrides: Partial<SessionEvent> & Pick<SessionEvent, "type" | "sessionId">): SessionEvent {
function makeEvent(
overrides: Partial<SessionEvent> & Pick<SessionEvent, 'type' | 'sessionId'>,
): SessionEvent {
return {
id: "evt-1",
id: 'evt-1',
payload: null,
direction: "inbound",
direction: 'inbound',
seqNum: 1,
createdAt: Date.now(),
...overrides,
};
}
}
// =============================================================================
// user / user_message
// =============================================================================
describe("toClientPayload — user message", () => {
test("maps user type with content", () => {
describe('toClientPayload — user message', () => {
test('maps user type with content', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-1",
payload: { content: "hello" },
});
const result = toClientPayload(event);
expect(result.type).toBe("user");
expect(result.session_id).toBe("sess-1");
expect((result as any).message.role).toBe("user");
expect((result as any).message.content).toBe("hello");
});
type: 'user',
sessionId: 'sess-1',
payload: { content: 'hello' },
})
const result = toClientPayload(event)
expect(result.type).toBe('user')
expect(result.session_id).toBe('sess-1')
expect((result as any).message.role).toBe('user')
expect((result as any).message.content).toBe('hello')
})
test("maps user_message type same as user", () => {
test('maps user_message type same as user', () => {
const event = makeEvent({
type: "user_message",
sessionId: "sess-2",
payload: { content: "world" },
});
const result = toClientPayload(event);
expect(result.type).toBe("user");
expect(result.session_id).toBe("sess-2");
});
type: 'user_message',
sessionId: 'sess-2',
payload: { content: 'world' },
})
const result = toClientPayload(event)
expect(result.type).toBe('user')
expect(result.session_id).toBe('sess-2')
})
test("falls back to message field when content is missing", () => {
test('falls back to message field when content is missing', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-3",
payload: { message: "fallback msg" },
});
const result = toClientPayload(event);
expect((result as any).message.content).toBe("fallback msg");
});
type: 'user',
sessionId: 'sess-3',
payload: { message: 'fallback msg' },
})
const result = toClientPayload(event)
expect((result as any).message.content).toBe('fallback msg')
})
test("falls back to empty string when both content and message missing", () => {
test('falls back to empty string when both content and message missing', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-4",
type: 'user',
sessionId: 'sess-4',
payload: {},
});
const result = toClientPayload(event);
expect((result as any).message.content).toBe("");
});
})
const result = toClientPayload(event)
expect((result as any).message.content).toBe('')
})
test("includes isSynthetic when true", () => {
test('includes isSynthetic when true', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-5",
payload: { content: "auto", isSynthetic: true },
});
const result = toClientPayload(event);
expect((result as any).isSynthetic).toBe(true);
});
type: 'user',
sessionId: 'sess-5',
payload: { content: 'auto', isSynthetic: true },
})
const result = toClientPayload(event)
expect((result as any).isSynthetic).toBe(true)
})
test("does not include isSynthetic when false", () => {
test('does not include isSynthetic when false', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-6",
payload: { content: "manual", isSynthetic: false },
});
const result = toClientPayload(event);
expect((result as any).isSynthetic).toBeUndefined();
});
type: 'user',
sessionId: 'sess-6',
payload: { content: 'manual', isSynthetic: false },
})
const result = toClientPayload(event)
expect((result as any).isSynthetic).toBeUndefined()
})
test("uses payload.uuid when present", () => {
test('uses payload.uuid when present', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-7",
payload: { content: "hi", uuid: "custom-uuid" },
});
const result = toClientPayload(event);
expect(result.uuid).toBe("custom-uuid");
});
type: 'user',
sessionId: 'sess-7',
payload: { content: 'hi', uuid: 'custom-uuid' },
})
const result = toClientPayload(event)
expect(result.uuid).toBe('custom-uuid')
})
test("falls back to event.id when payload.uuid is missing", () => {
test('falls back to event.id when payload.uuid is missing', () => {
const event = makeEvent({
type: "user",
sessionId: "sess-8",
payload: { content: "hi" },
});
const result = toClientPayload(event);
expect(result.uuid).toBe("evt-1");
});
});
type: 'user',
sessionId: 'sess-8',
payload: { content: 'hi' },
})
const result = toClientPayload(event)
expect(result.uuid).toBe('evt-1')
})
})
// =============================================================================
// permission_response / control_response
// =============================================================================
describe("toClientPayload — permission response", () => {
test("approved=true maps to allow behavior", () => {
describe('toClientPayload — permission response', () => {
test('approved=true maps to allow behavior', () => {
const event = makeEvent({
type: "permission_response",
sessionId: "sess-1",
payload: { approved: true, request_id: "req-1" },
});
const result = toClientPayload(event);
expect(result.type).toBe("control_response");
const resp = (result as any).response;
expect(resp.subtype).toBe("success");
expect(resp.request_id).toBe("req-1");
expect(resp.response.behavior).toBe("allow");
});
type: 'permission_response',
sessionId: 'sess-1',
payload: { approved: true, request_id: 'req-1' },
})
const result = toClientPayload(event)
expect(result.type).toBe('control_response')
const resp = (result as any).response
expect(resp.subtype).toBe('success')
expect(resp.request_id).toBe('req-1')
expect(resp.response.behavior).toBe('allow')
})
test("approved=false maps to deny behavior with error", () => {
test('approved=false maps to deny behavior with error', () => {
const event = makeEvent({
type: "permission_response",
sessionId: "sess-2",
payload: { approved: false, request_id: "req-2" },
});
const result = toClientPayload(event);
expect(result.type).toBe("control_response");
const resp = (result as any).response;
expect(resp.subtype).toBe("error");
expect(resp.error).toBe("Permission denied by user");
expect(resp.response.behavior).toBe("deny");
});
type: 'permission_response',
sessionId: 'sess-2',
payload: { approved: false, request_id: 'req-2' },
})
const result = toClientPayload(event)
expect(result.type).toBe('control_response')
const resp = (result as any).response
expect(resp.subtype).toBe('error')
expect(resp.error).toBe('Permission denied by user')
expect(resp.response.behavior).toBe('deny')
})
test("approved=false includes feedback message when provided", () => {
test('approved=false includes feedback message when provided', () => {
const event = makeEvent({
type: "permission_response",
sessionId: "sess-3",
payload: { approved: false, request_id: "req-3", message: "please revise" },
});
const result = toClientPayload(event);
expect((result as any).response.message).toBe("please revise");
});
type: 'permission_response',
sessionId: 'sess-3',
payload: {
approved: false,
request_id: 'req-3',
message: 'please revise',
},
})
const result = toClientPayload(event)
expect((result as any).response.message).toBe('please revise')
})
test("passes through existingResponse directly", () => {
const existingResponse = { subtype: "success", custom: true };
test('passes through existingResponse directly', () => {
const existingResponse = { subtype: 'success', custom: true }
const event = makeEvent({
type: "control_response",
sessionId: "sess-4",
type: 'control_response',
sessionId: 'sess-4',
payload: { approved: true, response: existingResponse },
});
const result = toClientPayload(event);
expect(result.type).toBe("control_response");
expect((result as any).response).toBe(existingResponse);
});
})
const result = toClientPayload(event)
expect(result.type).toBe('control_response')
expect((result as any).response).toBe(existingResponse)
})
test("includes updatedInput when approved with updated_input", () => {
const updatedInput = { file_path: "/new/path" };
test('includes updatedInput when approved with updated_input', () => {
const updatedInput = { file_path: '/new/path' }
const event = makeEvent({
type: "permission_response",
sessionId: "sess-5",
payload: { approved: true, request_id: "req-5", updated_input: updatedInput },
});
const result = toClientPayload(event);
expect((result as any).response.response.updatedInput).toEqual(updatedInput);
});
type: 'permission_response',
sessionId: 'sess-5',
payload: {
approved: true,
request_id: 'req-5',
updated_input: updatedInput,
},
})
const result = toClientPayload(event)
expect((result as any).response.response.updatedInput).toEqual(updatedInput)
})
test("includes updatedPermissions when approved with updated_permissions", () => {
const perms = [{ type: "allow", tool: "bash" }];
test('includes updatedPermissions when approved with updated_permissions', () => {
const perms = [{ type: 'allow', tool: 'bash' }]
const event = makeEvent({
type: "permission_response",
sessionId: "sess-6",
payload: { approved: true, request_id: "req-6", updated_permissions: perms },
});
const result = toClientPayload(event);
expect((result as any).response.response.updatedPermissions).toEqual(perms);
});
});
type: 'permission_response',
sessionId: 'sess-6',
payload: {
approved: true,
request_id: 'req-6',
updated_permissions: perms,
},
})
const result = toClientPayload(event)
expect((result as any).response.response.updatedPermissions).toEqual(perms)
})
})
// =============================================================================
// interrupt
// =============================================================================
describe("toClientPayload — interrupt", () => {
test("maps interrupt to control_request with subtype interrupt", () => {
describe('toClientPayload — interrupt', () => {
test('maps interrupt to control_request with subtype interrupt', () => {
const event = makeEvent({
type: "interrupt",
sessionId: "sess-1",
});
const result = toClientPayload(event);
expect(result.type).toBe("control_request");
expect((result as any).request_id).toBe("evt-1");
expect((result as any).request.subtype).toBe("interrupt");
});
});
type: 'interrupt',
sessionId: 'sess-1',
})
const result = toClientPayload(event)
expect(result.type).toBe('control_request')
expect((result as any).request_id).toBe('evt-1')
expect((result as any).request.subtype).toBe('interrupt')
})
})
// =============================================================================
// control_request
// =============================================================================
describe("toClientPayload — control_request", () => {
test("passes through request_id and request from payload", () => {
describe('toClientPayload — control_request', () => {
test('passes through request_id and request from payload', () => {
const event = makeEvent({
type: "control_request",
sessionId: "sess-1",
payload: { request_id: "req-99", request: { subtype: "permission", tool: "bash" } },
});
const result = toClientPayload(event);
expect(result.type).toBe("control_request");
expect((result as any).request_id).toBe("req-99");
expect((result as any).request.subtype).toBe("permission");
});
type: 'control_request',
sessionId: 'sess-1',
payload: {
request_id: 'req-99',
request: { subtype: 'permission', tool: 'bash' },
},
})
const result = toClientPayload(event)
expect(result.type).toBe('control_request')
expect((result as any).request_id).toBe('req-99')
expect((result as any).request.subtype).toBe('permission')
})
test("falls back request to payload when no request field", () => {
test('falls back request to payload when no request field', () => {
const event = makeEvent({
type: "control_request",
sessionId: "sess-2",
payload: { request_id: "req-10", custom: "data" },
});
const result = toClientPayload(event);
expect((result as any).request).toEqual({ request_id: "req-10", custom: "data" });
});
type: 'control_request',
sessionId: 'sess-2',
payload: { request_id: 'req-10', custom: 'data' },
})
const result = toClientPayload(event)
expect((result as any).request).toEqual({
request_id: 'req-10',
custom: 'data',
})
})
test("falls back request_id to event.id when missing", () => {
test('falls back request_id to event.id when missing', () => {
const event = makeEvent({
type: "control_request",
sessionId: "sess-3",
payload: { request: { subtype: "test" } },
});
const result = toClientPayload(event);
expect((result as any).request_id).toBe("evt-1");
});
});
type: 'control_request',
sessionId: 'sess-3',
payload: { request: { subtype: 'test' } },
})
const result = toClientPayload(event)
expect((result as any).request_id).toBe('evt-1')
})
})
// =============================================================================
// default fallback
// =============================================================================
describe("toClientPayload — default types", () => {
test("passes through unknown type with type/uuid/session_id/message", () => {
describe('toClientPayload — default types', () => {
test('passes through unknown type with type/uuid/session_id/message', () => {
const event = makeEvent({
type: "assistant",
sessionId: "sess-1",
payload: { uuid: "u-1", content: "response text" },
});
const result = toClientPayload(event);
expect(result.type).toBe("assistant");
expect(result.uuid).toBe("u-1");
expect(result.session_id).toBe("sess-1");
expect(result.message).toEqual({ uuid: "u-1", content: "response text" });
});
});
type: 'assistant',
sessionId: 'sess-1',
payload: { uuid: 'u-1', content: 'response text' },
})
const result = toClientPayload(event)
expect(result.type).toBe('assistant')
expect(result.uuid).toBe('u-1')
expect(result.session_id).toBe('sess-1')
expect(result.message).toEqual({ uuid: 'u-1', content: 'response text' })
})
})

View File

@@ -1,11 +1,11 @@
import { describe, test, expect, beforeEach, mock } from "bun:test";
import { describe, test, expect, beforeEach, mock } from 'bun:test'
// Mock config with very short timeout for testing
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
host: '0.0.0.0',
apiKeys: ['test-api-key'],
baseUrl: 'http://localhost:3000',
pollTimeout: 8,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
@@ -13,12 +13,12 @@ const mockConfig = {
webCorsOrigins: [],
wsIdleTimeout: 30,
wsKeepaliveInterval: 20,
};
}
mock.module("../config", () => ({
mock.module('../config', () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
getBaseUrl: () => 'http://localhost:3000',
}))
import {
storeReset,
@@ -28,84 +28,91 @@ import {
storeUpdateSession,
storeGetEnvironment,
storeGetSession,
} from "../store";
import { getEventBus, getAllEventBuses, removeEventBus } from "../transport/event-bus";
import { runDisconnectMonitorSweep } from "../services/disconnect-monitor";
} from '../store'
import {
getEventBus,
getAllEventBuses,
removeEventBus,
} from '../transport/event-bus'
import { runDisconnectMonitorSweep } from '../services/disconnect-monitor'
describe("Disconnect Monitor Logic", () => {
describe('Disconnect Monitor Logic', () => {
beforeEach(() => {
storeReset();
storeReset()
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
removeEventBus(key)
}
});
})
test("environment times out when lastPollAt is too old", () => {
const env = storeCreateEnvironment({ secret: "s" });
const timeoutMs = 300 * 1000; // 5 minutes
test('environment times out when lastPollAt is too old', () => {
const env = storeCreateEnvironment({ secret: 's' })
const timeoutMs = 300 * 1000 // 5 minutes
// Simulate lastPollAt being 6 minutes ago
const oldDate = new Date(Date.now() - timeoutMs - 60000);
storeUpdateEnvironment(env.id, { lastPollAt: oldDate });
const oldDate = new Date(Date.now() - timeoutMs - 60000)
storeUpdateEnvironment(env.id, { lastPollAt: oldDate })
runDisconnectMonitorSweep();
runDisconnectMonitorSweep()
const updated = storeGetEnvironment(env.id);
expect(updated?.status).toBe("disconnected");
});
const updated = storeGetEnvironment(env.id)
expect(updated?.status).toBe('disconnected')
})
test("environment stays active when lastPollAt is recent", () => {
const env = storeCreateEnvironment({ secret: "s" });
runDisconnectMonitorSweep();
test('environment stays active when lastPollAt is recent', () => {
const env = storeCreateEnvironment({ secret: 's' })
runDisconnectMonitorSweep()
const updated = storeGetEnvironment(env.id);
expect(updated?.status).toBe("active");
});
const updated = storeGetEnvironment(env.id)
expect(updated?.status).toBe('active')
})
test("session becomes inactive when updatedAt is too old", () => {
const session = storeCreateSession({});
storeUpdateSession(session.id, { status: "running" });
const rec = storeGetSession(session.id);
expect(rec).toBeTruthy();
if (!rec) return;
test('session becomes inactive when updatedAt is too old', () => {
const session = storeCreateSession({})
storeUpdateSession(session.id, { status: 'running' })
const rec = storeGetSession(session.id)
expect(rec).toBeTruthy()
if (!rec) return
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000)
runDisconnectMonitorSweep();
runDisconnectMonitorSweep()
const updated = storeGetSession(session.id);
expect(updated?.status).toBe("inactive");
});
const updated = storeGetSession(session.id)
expect(updated?.status).toBe('inactive')
})
test("session stays running when recently updated", () => {
const session = storeCreateSession({});
storeUpdateSession(session.id, { status: "running" });
test('session stays running when recently updated', () => {
const session = storeCreateSession({})
storeUpdateSession(session.id, { status: 'running' })
runDisconnectMonitorSweep();
runDisconnectMonitorSweep()
const updated = storeGetSession(session.id);
expect(updated?.status).toBe("running");
});
const updated = storeGetSession(session.id)
expect(updated?.status).toBe('running')
})
test("session timeout publishes an inactive session_status event", () => {
const session = storeCreateSession({});
storeUpdateSession(session.id, { status: "idle" });
const rec = storeGetSession(session.id);
expect(rec).toBeTruthy();
if (!rec) return;
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000);
test('session timeout publishes an inactive session_status event', () => {
const session = storeCreateSession({})
storeUpdateSession(session.id, { status: 'idle' })
const rec = storeGetSession(session.id)
expect(rec).toBeTruthy()
if (!rec) return
rec.updatedAt = new Date(Date.now() - 300 * 1000 * 2 - 60000)
const bus = getEventBus(session.id);
const events: Array<{ type: string; payload: { status?: string } }> = [];
bus.subscribe((event) => {
events.push({ type: event.type, payload: event.payload as { status?: string } });
});
const bus = getEventBus(session.id)
const events: Array<{ type: string; payload: { status?: string } }> = []
bus.subscribe(event => {
events.push({
type: event.type,
payload: event.payload as { status?: string },
})
})
runDisconnectMonitorSweep();
runDisconnectMonitorSweep()
expect(events).toContainEqual({
type: "session_status",
payload: { status: "inactive" },
});
});
});
type: 'session_status',
payload: { status: 'inactive' },
})
})
})

View File

@@ -1,176 +1,293 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { EventBus, getEventBus, removeEventBus, getAllEventBuses } from "../transport/event-bus";
import { describe, test, expect, beforeEach } from 'bun:test'
import {
EventBus,
getEventBus,
removeEventBus,
getAllEventBuses,
} from '../transport/event-bus'
describe("EventBus", () => {
let bus: EventBus;
describe('EventBus', () => {
let bus: EventBus
beforeEach(() => {
bus = new EventBus();
});
bus = new EventBus()
})
describe("publish", () => {
test("publishes event with seqNum starting at 1", () => {
describe('publish', () => {
test('publishes event with seqNum starting at 1', () => {
const event = bus.publish({
id: "e1",
sessionId: "s1",
type: "user",
payload: { content: "hello" },
direction: "outbound",
});
expect(event.seqNum).toBe(1);
expect(event.createdAt).toBeGreaterThan(0);
});
id: 'e1',
sessionId: 's1',
type: 'user',
payload: { content: 'hello' },
direction: 'outbound',
})
expect(event.seqNum).toBe(1)
expect(event.createdAt).toBeGreaterThan(0)
})
test("increments seqNum on each publish", () => {
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
bus.publish({ id: "e2", sessionId: "s1", type: "assistant", payload: {}, direction: "inbound" });
const event = bus.publish({ id: "e3", sessionId: "s1", type: "result", payload: {}, direction: "inbound" });
expect(event.seqNum).toBe(3);
});
test('increments seqNum on each publish', () => {
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
bus.publish({
id: 'e2',
sessionId: 's1',
type: 'assistant',
payload: {},
direction: 'inbound',
})
const event = bus.publish({
id: 'e3',
sessionId: 's1',
type: 'result',
payload: {},
direction: 'inbound',
})
expect(event.seqNum).toBe(3)
})
test("throws when publishing to a closed bus", () => {
bus.close();
test('throws when publishing to a closed bus', () => {
bus.close()
expect(() =>
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" }),
).toThrow("EventBus is closed");
});
});
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
}),
).toThrow('EventBus is closed')
})
})
describe("subscribe", () => {
test("receives published events", () => {
const received: unknown[] = [];
bus.subscribe((event) => received.push(event));
describe('subscribe', () => {
test('receives published events', () => {
const received: unknown[] = []
bus.subscribe(event => received.push(event))
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: { content: "hi" }, direction: "outbound" });
expect(received).toHaveLength(1);
expect((received[0] as any).payload).toEqual({ content: "hi" });
});
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: { content: 'hi' },
direction: 'outbound',
})
expect(received).toHaveLength(1)
expect((received[0] as any).payload).toEqual({ content: 'hi' })
})
test("unsubscribe stops receiving events", () => {
const received: unknown[] = [];
const unsub = bus.subscribe((event) => received.push(event));
unsub();
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
expect(received).toHaveLength(0);
});
test('unsubscribe stops receiving events', () => {
const received: unknown[] = []
const unsub = bus.subscribe(event => received.push(event))
unsub()
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
expect(received).toHaveLength(0)
})
test("multiple subscribers all receive events", () => {
const r1: unknown[] = [];
const r2: unknown[] = [];
bus.subscribe((e) => r1.push(e));
bus.subscribe((e) => r2.push(e));
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
expect(r1).toHaveLength(1);
expect(r2).toHaveLength(1);
});
test('multiple subscribers all receive events', () => {
const r1: unknown[] = []
const r2: unknown[] = []
bus.subscribe(e => r1.push(e))
bus.subscribe(e => r2.push(e))
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
expect(r1).toHaveLength(1)
expect(r2).toHaveLength(1)
})
test("subscriber error does not affect other subscribers", () => {
const received: unknown[] = [];
test('subscriber error does not affect other subscribers', () => {
const received: unknown[] = []
bus.subscribe(() => {
throw new Error("boom");
});
bus.subscribe((e) => received.push(e));
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
expect(received).toHaveLength(1);
});
throw new Error('boom')
})
bus.subscribe(e => received.push(e))
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
expect(received).toHaveLength(1)
})
test("subscriberCount", () => {
expect(bus.subscriberCount()).toBe(0);
const unsub1 = bus.subscribe(() => {});
expect(bus.subscriberCount()).toBe(1);
const unsub2 = bus.subscribe(() => {});
expect(bus.subscriberCount()).toBe(2);
unsub1();
expect(bus.subscriberCount()).toBe(1);
});
});
test('subscriberCount', () => {
expect(bus.subscriberCount()).toBe(0)
const unsub1 = bus.subscribe(() => {})
expect(bus.subscriberCount()).toBe(1)
const unsub2 = bus.subscribe(() => {})
expect(bus.subscriberCount()).toBe(2)
unsub1()
expect(bus.subscriberCount()).toBe(1)
})
})
describe("getEventsSince", () => {
test("returns events after given seqNum", () => {
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
bus.publish({ id: "e2", sessionId: "s1", type: "assistant", payload: {}, direction: "inbound" });
bus.publish({ id: "e3", sessionId: "s1", type: "result", payload: {}, direction: "inbound" });
describe('getEventsSince', () => {
test('returns events after given seqNum', () => {
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
bus.publish({
id: 'e2',
sessionId: 's1',
type: 'assistant',
payload: {},
direction: 'inbound',
})
bus.publish({
id: 'e3',
sessionId: 's1',
type: 'result',
payload: {},
direction: 'inbound',
})
const events = bus.getEventsSince(1);
expect(events).toHaveLength(2);
expect(events[0].seqNum).toBe(2);
expect(events[1].seqNum).toBe(3);
});
const events = bus.getEventsSince(1)
expect(events).toHaveLength(2)
expect(events[0].seqNum).toBe(2)
expect(events[1].seqNum).toBe(3)
})
test("returns empty for seqNum beyond last", () => {
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
expect(bus.getEventsSince(1)).toHaveLength(0);
});
test('returns empty for seqNum beyond last', () => {
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
expect(bus.getEventsSince(1)).toHaveLength(0)
})
test("returns all events when seqNum is 0", () => {
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
bus.publish({ id: "e2", sessionId: "s1", type: "assistant", payload: {}, direction: "inbound" });
expect(bus.getEventsSince(0)).toHaveLength(2);
});
});
test('returns all events when seqNum is 0', () => {
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
bus.publish({
id: 'e2',
sessionId: 's1',
type: 'assistant',
payload: {},
direction: 'inbound',
})
expect(bus.getEventsSince(0)).toHaveLength(2)
})
})
describe("getLastSeqNum", () => {
test("returns 0 for empty bus", () => {
expect(bus.getLastSeqNum()).toBe(0);
});
describe('getLastSeqNum', () => {
test('returns 0 for empty bus', () => {
expect(bus.getLastSeqNum()).toBe(0)
})
test("returns last seqNum after publishes", () => {
bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
bus.publish({ id: "e2", sessionId: "s1", type: "user", payload: {}, direction: "outbound" });
expect(bus.getLastSeqNum()).toBe(2);
});
});
test('returns last seqNum after publishes', () => {
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
bus.publish({
id: 'e2',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
})
expect(bus.getLastSeqNum()).toBe(2)
})
})
describe("close", () => {
test("clears subscribers and prevents publishing", () => {
bus.subscribe(() => {});
bus.close();
expect(bus.subscriberCount()).toBe(0);
expect(() => bus.publish({ id: "e1", sessionId: "s1", type: "user", payload: {}, direction: "outbound" })).toThrow();
});
});
});
describe('close', () => {
test('clears subscribers and prevents publishing', () => {
bus.subscribe(() => {})
bus.close()
expect(bus.subscriberCount()).toBe(0)
expect(() =>
bus.publish({
id: 'e1',
sessionId: 's1',
type: 'user',
payload: {},
direction: 'outbound',
}),
).toThrow()
})
})
})
describe("EventBus registry", () => {
describe('EventBus registry', () => {
beforeEach(() => {
// Clean up global registry
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
removeEventBus(key)
}
});
})
describe("getEventBus", () => {
test("creates new bus for unknown session", () => {
const bus = getEventBus("s1");
expect(bus).toBeInstanceOf(EventBus);
expect(getAllEventBuses().has("s1")).toBe(true);
});
describe('getEventBus', () => {
test('creates new bus for unknown session', () => {
const bus = getEventBus('s1')
expect(bus).toBeInstanceOf(EventBus)
expect(getAllEventBuses().has('s1')).toBe(true)
})
test("returns same bus for same session", () => {
const bus1 = getEventBus("s1");
const bus2 = getEventBus("s1");
expect(bus1).toBe(bus2);
});
});
test('returns same bus for same session', () => {
const bus1 = getEventBus('s1')
const bus2 = getEventBus('s1')
expect(bus1).toBe(bus2)
})
})
describe("removeEventBus", () => {
test("removes and closes bus", () => {
const bus = getEventBus("s2");
removeEventBus("s2");
expect(getAllEventBuses().has("s2")).toBe(false);
expect(() => bus.publish({ id: "e1", sessionId: "s2", type: "user", payload: {}, direction: "outbound" })).toThrow();
});
describe('removeEventBus', () => {
test('removes and closes bus', () => {
const bus = getEventBus('s2')
removeEventBus('s2')
expect(getAllEventBuses().has('s2')).toBe(false)
expect(() =>
bus.publish({
id: 'e1',
sessionId: 's2',
type: 'user',
payload: {},
direction: 'outbound',
}),
).toThrow()
})
test("no-op for non-existent bus", () => {
expect(() => removeEventBus("nonexistent")).not.toThrow();
});
});
test('no-op for non-existent bus', () => {
expect(() => removeEventBus('nonexistent')).not.toThrow()
})
})
describe("getAllEventBuses", () => {
test("returns all registered buses", () => {
getEventBus("a");
getEventBus("b");
expect(getAllEventBuses().size).toBeGreaterThanOrEqual(2);
});
});
});
describe('getAllEventBuses', () => {
test('returns all registered buses', () => {
getEventBus('a')
getEventBus('b')
expect(getAllEventBuses().size).toBeGreaterThanOrEqual(2)
})
})
})

View File

@@ -1,28 +1,28 @@
import { describe, test, expect, beforeEach, afterAll, mock } from "bun:test";
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test'
// Mock config before imports
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
host: '0.0.0.0',
apiKeys: ['test-api-key'],
baseUrl: 'http://localhost:3000',
pollTimeout: 8,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
disconnectTimeout: 300,
webCorsOrigins: ["https://dashboard.example"],
webCorsOrigins: ['https://dashboard.example'],
wsIdleTimeout: 30,
wsKeepaliveInterval: 20,
};
}
mock.module("../config", () => ({
mock.module('../config', () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
getBaseUrl: () => 'http://localhost:3000',
}))
import { Hono } from "hono";
import { cors } from "hono/cors";
import { storeReset, storeCreateUser } from "../store";
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { storeReset, storeCreateUser } from '../store'
import {
apiKeyAuth,
encodeWebSocketAuthProtocol,
@@ -30,266 +30,266 @@ import {
sessionIngressAuth,
uuidAuth,
getUuidFromRequest,
} from "../auth/middleware";
import { issueToken } from "../auth/token";
import { generateWorkerJwt } from "../auth/jwt";
} from '../auth/middleware'
import { issueToken } from '../auth/token'
import { generateWorkerJwt } from '../auth/jwt'
import {
getAllowedWebCorsOrigins,
resolveWebCorsOrigin,
webCorsOptions,
} from "../auth/cors";
} from '../auth/cors'
// Helper: create a test app with middleware and a simple handler
function createTestApp() {
const app = new Hono();
const app = new Hono()
// Test route for apiKeyAuth
app.get("/api-key-test", apiKeyAuth, (c) => {
return c.json({ username: c.get("username") || null });
});
app.get('/api-key-test', apiKeyAuth, c => {
return c.json({ username: c.get('username') || null })
})
// Test route for sessionIngressAuth
app.get("/ingress/:id", sessionIngressAuth, (c) => {
return c.json({ ok: true, jwtPayload: c.get("jwtPayload") || null });
});
app.get('/ingress/:id', sessionIngressAuth, c => {
return c.json({ ok: true, jwtPayload: c.get('jwtPayload') || null })
})
// Test route for uuidAuth
app.get("/uuid-test", uuidAuth, (c) => {
return c.json({ uuid: c.get("uuid") });
});
app.get('/uuid-test', uuidAuth, c => {
return c.json({ uuid: c.get('uuid') })
})
// Test route for getUuidFromRequest
app.get("/uuid-extract", (c) => {
return c.json({ uuid: getUuidFromRequest(c) });
});
app.get('/uuid-extract', c => {
return c.json({ uuid: getUuidFromRequest(c) })
})
app.get("/ws-auth-token", (c) => {
return c.json({ token: extractWebSocketAuthToken(c) ?? null });
});
app.get('/ws-auth-token', c => {
return c.json({ token: extractWebSocketAuthToken(c) ?? null })
})
return app;
return app
}
describe("Auth Middleware", () => {
let app: Hono;
describe('Auth Middleware', () => {
let app: Hono
beforeEach(() => {
storeReset();
app = createTestApp();
});
storeReset()
app = createTestApp()
})
describe("apiKeyAuth", () => {
test("accepts valid API key with username header", async () => {
const res = await app.request("/api-key-test", {
describe('apiKeyAuth', () => {
test('accepts valid API key with username header', async () => {
const res = await app.request('/api-key-test', {
headers: {
Authorization: "Bearer test-api-key",
"X-Username": "alice",
Authorization: 'Bearer test-api-key',
'X-Username': 'alice',
},
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.username).toBe("alice");
});
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.username).toBe('alice')
})
test("accepts valid API key with username query param", async () => {
const res = await app.request("/api-key-test?username=bob", {
headers: { Authorization: "Bearer test-api-key" },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.username).toBe("bob");
});
test('accepts valid API key with username query param', async () => {
const res = await app.request('/api-key-test?username=bob', {
headers: { Authorization: 'Bearer test-api-key' },
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.username).toBe('bob')
})
test("accepts valid session token", async () => {
storeCreateUser("charlie");
const { token } = issueToken("charlie");
const res = await app.request("/api-key-test", {
test('accepts valid session token', async () => {
storeCreateUser('charlie')
const { token } = issueToken('charlie')
const res = await app.request('/api-key-test', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.username).toBe("charlie");
});
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.username).toBe('charlie')
})
test("rejects invalid token", async () => {
const res = await app.request("/api-key-test", {
headers: { Authorization: "Bearer wrong-key" },
});
expect(res.status).toBe(401);
});
test('rejects invalid token', async () => {
const res = await app.request('/api-key-test', {
headers: { Authorization: 'Bearer wrong-key' },
})
expect(res.status).toBe(401)
})
test("rejects missing token", async () => {
const res = await app.request("/api-key-test");
expect(res.status).toBe(401);
});
test('rejects missing token', async () => {
const res = await app.request('/api-key-test')
expect(res.status).toBe(401)
})
test("rejects session token from query param", async () => {
storeCreateUser("dave");
const { token } = issueToken("dave");
const res = await app.request(`/api-key-test?token=${token}`);
expect(res.status).toBe(401);
});
});
test('rejects session token from query param', async () => {
storeCreateUser('dave')
const { token } = issueToken('dave')
const res = await app.request(`/api-key-test?token=${token}`)
expect(res.status).toBe(401)
})
})
describe("sessionIngressAuth", () => {
const originalKeys = process.env.RCS_API_KEYS;
describe('sessionIngressAuth', () => {
const originalKeys = process.env.RCS_API_KEYS
beforeEach(() => {
process.env.RCS_API_KEYS = "test-api-key";
});
process.env.RCS_API_KEYS = 'test-api-key'
})
afterAll(() => {
process.env.RCS_API_KEYS = originalKeys;
});
process.env.RCS_API_KEYS = originalKeys
})
test("accepts valid API key", async () => {
const res = await app.request("/ingress/ses_123", {
headers: { Authorization: "Bearer test-api-key" },
});
expect(res.status).toBe(200);
});
test('accepts valid API key', async () => {
const res = await app.request('/ingress/ses_123', {
headers: { Authorization: 'Bearer test-api-key' },
})
expect(res.status).toBe(200)
})
test("accepts API key from WebSocket protocol header", async () => {
const res = await app.request("/ingress/ses_123", {
test('accepts API key from WebSocket protocol header', async () => {
const res = await app.request('/ingress/ses_123', {
headers: {
"Sec-WebSocket-Protocol": encodeWebSocketAuthProtocol("test-api-key"),
'Sec-WebSocket-Protocol': encodeWebSocketAuthProtocol('test-api-key'),
},
});
expect(res.status).toBe(200);
});
})
expect(res.status).toBe(200)
})
test("accepts valid JWT with matching session_id", async () => {
const jwt = generateWorkerJwt("ses_123", 3600);
const res = await app.request("/ingress/ses_123", {
test('accepts valid JWT with matching session_id', async () => {
const jwt = generateWorkerJwt('ses_123', 3600)
const res = await app.request('/ingress/ses_123', {
headers: { Authorization: `Bearer ${jwt}` },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.jwtPayload).not.toBeNull();
expect(body.jwtPayload.session_id).toBe("ses_123");
});
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.jwtPayload).not.toBeNull()
expect(body.jwtPayload.session_id).toBe('ses_123')
})
test("rejects JWT with mismatched session_id", async () => {
const jwt = generateWorkerJwt("ses_456", 3600);
const res = await app.request("/ingress/ses_123", {
test('rejects JWT with mismatched session_id', async () => {
const jwt = generateWorkerJwt('ses_456', 3600)
const res = await app.request('/ingress/ses_123', {
headers: { Authorization: `Bearer ${jwt}` },
});
expect(res.status).toBe(403);
});
})
expect(res.status).toBe(403)
})
test("rejects missing token", async () => {
const res = await app.request("/ingress/ses_123");
expect(res.status).toBe(401);
});
test('rejects missing token', async () => {
const res = await app.request('/ingress/ses_123')
expect(res.status).toBe(401)
})
test("rejects invalid token", async () => {
const res = await app.request("/ingress/ses_123", {
headers: { Authorization: "Bearer invalid" },
});
expect(res.status).toBe(401);
});
});
test('rejects invalid token', async () => {
const res = await app.request('/ingress/ses_123', {
headers: { Authorization: 'Bearer invalid' },
})
expect(res.status).toBe(401)
})
})
describe("extractWebSocketAuthToken", () => {
test("does not read tokens from query params", async () => {
const res = await app.request("/ws-auth-token?token=test-api-key");
const body = await res.json();
expect(body.token).toBeNull();
});
describe('extractWebSocketAuthToken', () => {
test('does not read tokens from query params', async () => {
const res = await app.request('/ws-auth-token?token=test-api-key')
const body = await res.json()
expect(body.token).toBeNull()
})
test("reads tokens from WebSocket protocol header", async () => {
const res = await app.request("/ws-auth-token", {
test('reads tokens from WebSocket protocol header', async () => {
const res = await app.request('/ws-auth-token', {
headers: {
"Sec-WebSocket-Protocol": encodeWebSocketAuthProtocol("test-api-key"),
'Sec-WebSocket-Protocol': encodeWebSocketAuthProtocol('test-api-key'),
},
});
const body = await res.json();
expect(body.token).toBe("test-api-key");
});
});
})
const body = await res.json()
expect(body.token).toBe('test-api-key')
})
})
describe("uuidAuth", () => {
test("accepts UUID from query param", async () => {
const res = await app.request("/uuid-test?uuid=test-uuid-1");
expect(res.status).toBe(200);
const body = await res.json();
expect(body.uuid).toBe("test-uuid-1");
});
describe('uuidAuth', () => {
test('accepts UUID from query param', async () => {
const res = await app.request('/uuid-test?uuid=test-uuid-1')
expect(res.status).toBe(200)
const body = await res.json()
expect(body.uuid).toBe('test-uuid-1')
})
test("accepts UUID from header", async () => {
const res = await app.request("/uuid-test", {
headers: { "X-UUID": "test-uuid-2" },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.uuid).toBe("test-uuid-2");
});
test('accepts UUID from header', async () => {
const res = await app.request('/uuid-test', {
headers: { 'X-UUID': 'test-uuid-2' },
})
expect(res.status).toBe(200)
const body = await res.json()
expect(body.uuid).toBe('test-uuid-2')
})
test("rejects missing UUID", async () => {
const res = await app.request("/uuid-test");
expect(res.status).toBe(401);
});
});
test('rejects missing UUID', async () => {
const res = await app.request('/uuid-test')
expect(res.status).toBe(401)
})
})
describe("getUuidFromRequest", () => {
test("extracts from query param", async () => {
const res = await app.request("/uuid-extract?uuid=from-query");
const body = await res.json();
expect(body.uuid).toBe("from-query");
});
describe('getUuidFromRequest', () => {
test('extracts from query param', async () => {
const res = await app.request('/uuid-extract?uuid=from-query')
const body = await res.json()
expect(body.uuid).toBe('from-query')
})
test("extracts from header", async () => {
const res = await app.request("/uuid-extract", {
headers: { "X-UUID": "from-header" },
});
const body = await res.json();
expect(body.uuid).toBe("from-header");
});
test('extracts from header', async () => {
const res = await app.request('/uuid-extract', {
headers: { 'X-UUID': 'from-header' },
})
const body = await res.json()
expect(body.uuid).toBe('from-header')
})
test("returns undefined when no UUID", async () => {
const res = await app.request("/uuid-extract");
const body = await res.json();
expect(body.uuid).toBeUndefined();
});
});
});
test('returns undefined when no UUID', async () => {
const res = await app.request('/uuid-extract')
const body = await res.json()
expect(body.uuid).toBeUndefined()
})
})
})
describe("Web CORS", () => {
describe('Web CORS', () => {
function createCorsApp() {
const corsApp = new Hono();
corsApp.use("/web/*", cors(webCorsOptions));
corsApp.get("/web/ping", (c) => c.text("ok"));
return corsApp;
const corsApp = new Hono()
corsApp.use('/web/*', cors(webCorsOptions))
corsApp.get('/web/ping', c => c.text('ok'))
return corsApp
}
test("allows configured origins plus local server origins", () => {
expect(getAllowedWebCorsOrigins()).toContain("https://dashboard.example");
expect(getAllowedWebCorsOrigins()).toContain("http://localhost:3000");
expect(getAllowedWebCorsOrigins()).toContain("http://127.0.0.1:3000");
expect(resolveWebCorsOrigin("https://dashboard.example")).toBe(
"https://dashboard.example",
);
});
test('allows configured origins plus local server origins', () => {
expect(getAllowedWebCorsOrigins()).toContain('https://dashboard.example')
expect(getAllowedWebCorsOrigins()).toContain('http://localhost:3000')
expect(getAllowedWebCorsOrigins()).toContain('http://127.0.0.1:3000')
expect(resolveWebCorsOrigin('https://dashboard.example')).toBe(
'https://dashboard.example',
)
})
test("rejects unknown origins by default", () => {
expect(resolveWebCorsOrigin("https://attacker.example")).toBeUndefined();
});
test('rejects unknown origins by default', () => {
expect(resolveWebCorsOrigin('https://attacker.example')).toBeUndefined()
})
test("does not emit CORS allow-origin for unknown web origins", async () => {
const res = await createCorsApp().request("/web/ping", {
headers: { Origin: "https://attacker.example" },
});
test('does not emit CORS allow-origin for unknown web origins', async () => {
const res = await createCorsApp().request('/web/ping', {
headers: { Origin: 'https://attacker.example' },
})
expect(res.status).toBe(200);
expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull();
});
expect(res.status).toBe(200)
expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull()
})
test("emits CORS allow-origin for configured web origins", async () => {
const res = await createCorsApp().request("/web/ping", {
headers: { Origin: "https://dashboard.example" },
});
test('emits CORS allow-origin for configured web origins', async () => {
const res = await createCorsApp().request('/web/ping', {
headers: { Origin: 'https://dashboard.example' },
})
expect(res.status).toBe(200);
expect(res.headers.get("Access-Control-Allow-Origin")).toBe(
"https://dashboard.example",
);
});
});
expect(res.status).toBe(200)
expect(res.headers.get('Access-Control-Allow-Origin')).toBe(
'https://dashboard.example',
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
import { describe, test, expect, beforeEach, mock } from "bun:test";
import { describe, test, expect, beforeEach, mock } from 'bun:test'
// Mock config before importing modules
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
host: '0.0.0.0',
apiKeys: ['test-api-key'],
baseUrl: 'http://localhost:3000',
pollTimeout: 8,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
@@ -13,14 +13,14 @@ const mockConfig = {
webCorsOrigins: [],
wsIdleTimeout: 30,
wsKeepaliveInterval: 20,
};
}
mock.module("../config", () => ({
mock.module('../config', () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
getBaseUrl: () => 'http://localhost:3000',
}))
import { storeReset, storeCreateEnvironment } from "../store";
import { storeReset, storeCreateEnvironment } from '../store'
import {
createSession,
createCodeSession,
@@ -33,7 +33,7 @@ import {
listSessionSummaries,
listSessionSummariesByUsername,
listSessionsByEnvironment,
} from "../services/session";
} from '../services/session'
import {
registerEnvironment,
deregisterEnvironment,
@@ -43,385 +43,404 @@ import {
listActiveEnvironmentsResponse,
listActiveEnvironmentsByUsername,
reconnectEnvironment,
} from "../services/environment";
import { normalizePayload, publishSessionEvent } from "../services/transport";
import { getEventBus, removeEventBus, getAllEventBuses } from "../transport/event-bus";
} from '../services/environment'
import { normalizePayload, publishSessionEvent } from '../services/transport'
import {
getEventBus,
removeEventBus,
getAllEventBuses,
} from '../transport/event-bus'
// ---------- Session Service ----------
describe("Session Service", () => {
describe('Session Service', () => {
beforeEach(() => {
storeReset();
storeReset()
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
removeEventBus(key)
}
});
})
describe("createSession", () => {
test("creates a session with defaults", () => {
const resp = createSession({});
expect(resp.id).toMatch(/^session_/);
expect(resp.status).toBe("idle");
expect(resp.source).toBe("remote-control");
expect(resp.environment_id).toBeNull();
expect(resp.worker_epoch).toBe(0);
expect(resp.created_at).toBeGreaterThan(0);
});
describe('createSession', () => {
test('creates a session with defaults', () => {
const resp = createSession({})
expect(resp.id).toMatch(/^session_/)
expect(resp.status).toBe('idle')
expect(resp.source).toBe('remote-control')
expect(resp.environment_id).toBeNull()
expect(resp.worker_epoch).toBe(0)
expect(resp.created_at).toBeGreaterThan(0)
})
test("creates a session with all options", () => {
const env = storeCreateEnvironment({ secret: "s" });
test('creates a session with all options', () => {
const env = storeCreateEnvironment({ secret: 's' })
const resp = createSession({
environment_id: env.id,
title: "My Session",
source: "cli",
permission_mode: "auto",
});
expect(resp.environment_id).toBe(env.id);
expect(resp.title).toBe("My Session");
expect(resp.source).toBe("cli");
expect(resp.permission_mode).toBe("auto");
});
title: 'My Session',
source: 'cli',
permission_mode: 'auto',
})
expect(resp.environment_id).toBe(env.id)
expect(resp.title).toBe('My Session')
expect(resp.source).toBe('cli')
expect(resp.permission_mode).toBe('auto')
})
test("creates session with username", () => {
const resp = createSession({ username: "alice" });
expect(resp.username).toBe("alice");
});
});
test('creates session with username', () => {
const resp = createSession({ username: 'alice' })
expect(resp.username).toBe('alice')
})
})
describe("createCodeSession", () => {
test("creates a code session with cse_ prefix", () => {
const resp = createCodeSession({});
expect(resp.id).toMatch(/^cse_/);
});
});
describe('createCodeSession', () => {
test('creates a code session with cse_ prefix', () => {
const resp = createCodeSession({})
expect(resp.id).toMatch(/^cse_/)
})
})
describe("getSession", () => {
test("returns null for non-existent session", () => {
expect(getSession("nope")).toBeNull();
});
describe('getSession', () => {
test('returns null for non-existent session', () => {
expect(getSession('nope')).toBeNull()
})
test("returns created session", () => {
const created = createSession({});
const fetched = getSession(created.id);
expect(fetched).not.toBeNull();
expect(fetched!.id).toBe(created.id);
});
});
test('returns created session', () => {
const created = createSession({})
const fetched = getSession(created.id)
expect(fetched).not.toBeNull()
expect(fetched!.id).toBe(created.id)
})
})
describe("updateSessionTitle", () => {
test("updates title", () => {
const s = createSession({});
updateSessionTitle(s.id, "New Title");
expect(getSession(s.id)?.title).toBe("New Title");
});
});
describe('updateSessionTitle', () => {
test('updates title', () => {
const s = createSession({})
updateSessionTitle(s.id, 'New Title')
expect(getSession(s.id)?.title).toBe('New Title')
})
})
describe("updateSessionStatus", () => {
test("updates status", () => {
const s = createSession({});
updateSessionStatus(s.id, "active");
expect(getSession(s.id)?.status).toBe("active");
});
});
describe('updateSessionStatus', () => {
test('updates status', () => {
const s = createSession({})
updateSessionStatus(s.id, 'active')
expect(getSession(s.id)?.status).toBe('active')
})
})
describe("archiveSession", () => {
test("sets status to archived and removes event bus", () => {
const s = createSession({});
describe('archiveSession', () => {
test('sets status to archived and removes event bus', () => {
const s = createSession({})
// Create event bus for this session
getEventBus(s.id);
archiveSession(s.id);
expect(getSession(s.id)?.status).toBe("archived");
expect(getAllEventBuses().has(s.id)).toBe(false);
});
});
getEventBus(s.id)
archiveSession(s.id)
expect(getSession(s.id)?.status).toBe('archived')
expect(getAllEventBuses().has(s.id)).toBe(false)
})
})
describe("incrementEpoch", () => {
test("increments epoch by 1", () => {
const s = createSession({});
expect(incrementEpoch(s.id)).toBe(1);
expect(incrementEpoch(s.id)).toBe(2);
expect(getSession(s.id)?.worker_epoch).toBe(2);
});
describe('incrementEpoch', () => {
test('increments epoch by 1', () => {
const s = createSession({})
expect(incrementEpoch(s.id)).toBe(1)
expect(incrementEpoch(s.id)).toBe(2)
expect(getSession(s.id)?.worker_epoch).toBe(2)
})
test("throws for non-existent session", () => {
expect(() => incrementEpoch("nope")).toThrow("Session not found");
});
});
test('throws for non-existent session', () => {
expect(() => incrementEpoch('nope')).toThrow('Session not found')
})
})
describe("listSessions", () => {
test("returns all sessions", () => {
createSession({});
createSession({});
expect(listSessions()).toHaveLength(2);
});
});
describe('listSessions', () => {
test('returns all sessions', () => {
createSession({})
createSession({})
expect(listSessions()).toHaveLength(2)
})
})
describe("listSessionSummaries", () => {
test("returns summaries with correct fields", () => {
createSession({ title: "Test" });
const summaries = listSessionSummaries();
expect(summaries).toHaveLength(1);
expect(summaries[0].title).toBe("Test");
expect(summaries[0].updated_at).toBeGreaterThan(0);
describe('listSessionSummaries', () => {
test('returns summaries with correct fields', () => {
createSession({ title: 'Test' })
const summaries = listSessionSummaries()
expect(summaries).toHaveLength(1)
expect(summaries[0].title).toBe('Test')
expect(summaries[0].updated_at).toBeGreaterThan(0)
// Summary should not have environment_id
expect("environment_id" in summaries[0]).toBe(false);
});
});
expect('environment_id' in summaries[0]).toBe(false)
})
})
describe("listSessionSummariesByUsername", () => {
test("filters by username", () => {
createSession({ username: "alice" });
createSession({ username: "bob" });
expect(listSessionSummariesByUsername("alice")).toHaveLength(1);
});
});
describe('listSessionSummariesByUsername', () => {
test('filters by username', () => {
createSession({ username: 'alice' })
createSession({ username: 'bob' })
expect(listSessionSummariesByUsername('alice')).toHaveLength(1)
})
})
describe("listSessionsByEnvironment", () => {
test("filters by environment", () => {
const env = storeCreateEnvironment({ secret: "s" });
createSession({ environment_id: env.id });
createSession({});
expect(listSessionsByEnvironment(env.id)).toHaveLength(1);
});
});
});
describe('listSessionsByEnvironment', () => {
test('filters by environment', () => {
const env = storeCreateEnvironment({ secret: 's' })
createSession({ environment_id: env.id })
createSession({})
expect(listSessionsByEnvironment(env.id)).toHaveLength(1)
})
})
})
// ---------- Environment Service ----------
describe("Environment Service", () => {
describe('Environment Service', () => {
beforeEach(() => {
storeReset();
});
storeReset()
})
describe("registerEnvironment", () => {
test("registers environment with defaults", () => {
const result = registerEnvironment({});
expect(result.environment_id).toMatch(/^env_/);
expect(result.environment_secret).toBe("test-api-key");
expect(result.status).toBe("active");
});
describe('registerEnvironment', () => {
test('registers environment with defaults', () => {
const result = registerEnvironment({})
expect(result.environment_id).toMatch(/^env_/)
expect(result.environment_secret).toBe('test-api-key')
expect(result.status).toBe('active')
})
test("registers with options", () => {
test('registers with options', () => {
const result = registerEnvironment({
machine_name: "mac1",
directory: "/home/user",
branch: "main",
git_repo_url: "https://github.com/test/repo",
machine_name: 'mac1',
directory: '/home/user',
branch: 'main',
git_repo_url: 'https://github.com/test/repo',
max_sessions: 5,
worker_type: "custom",
});
const env = getEnvironment(result.environment_id);
expect(env?.machineName).toBe("mac1");
expect(env?.directory).toBe("/home/user");
expect(env?.maxSessions).toBe(5);
});
worker_type: 'custom',
})
const env = getEnvironment(result.environment_id)
expect(env?.machineName).toBe('mac1')
expect(env?.directory).toBe('/home/user')
expect(env?.maxSessions).toBe(5)
})
test("registers with username", () => {
const result = registerEnvironment({ username: "alice" });
const env = getEnvironment(result.environment_id);
expect(env?.username).toBe("alice");
});
});
test('registers with username', () => {
const result = registerEnvironment({ username: 'alice' })
const env = getEnvironment(result.environment_id)
expect(env?.username).toBe('alice')
})
})
describe("deregisterEnvironment", () => {
test("sets status to deregistered", () => {
const result = registerEnvironment({});
deregisterEnvironment(result.environment_id);
const env = getEnvironment(result.environment_id);
expect(env?.status).toBe("deregistered");
});
});
describe('deregisterEnvironment', () => {
test('sets status to deregistered', () => {
const result = registerEnvironment({})
deregisterEnvironment(result.environment_id)
const env = getEnvironment(result.environment_id)
expect(env?.status).toBe('deregistered')
})
})
describe("updatePollTime", () => {
test("updates lastPollAt", () => {
const result = registerEnvironment({});
const before = getEnvironment(result.environment_id)?.lastPollAt;
describe('updatePollTime', () => {
test('updates lastPollAt', () => {
const result = registerEnvironment({})
const before = getEnvironment(result.environment_id)?.lastPollAt
// Small delay to ensure time difference
updatePollTime(result.environment_id);
const after = getEnvironment(result.environment_id)?.lastPollAt;
expect(after!.getTime()).toBeGreaterThanOrEqual(before!.getTime());
});
});
updatePollTime(result.environment_id)
const after = getEnvironment(result.environment_id)?.lastPollAt
expect(after!.getTime()).toBeGreaterThanOrEqual(before!.getTime())
})
})
describe("listActiveEnvironments", () => {
test("returns active environments", () => {
registerEnvironment({});
registerEnvironment({});
expect(listActiveEnvironments()).toHaveLength(2);
});
});
describe('listActiveEnvironments', () => {
test('returns active environments', () => {
registerEnvironment({})
registerEnvironment({})
expect(listActiveEnvironments()).toHaveLength(2)
})
})
describe("listActiveEnvironmentsResponse", () => {
test("returns response format", () => {
registerEnvironment({ machine_name: "mac1" });
const envs = listActiveEnvironmentsResponse();
expect(envs).toHaveLength(1);
expect(envs[0].machine_name).toBe("mac1");
expect(envs[0].last_poll_at).toBeGreaterThan(0);
});
});
describe('listActiveEnvironmentsResponse', () => {
test('returns response format', () => {
registerEnvironment({ machine_name: 'mac1' })
const envs = listActiveEnvironmentsResponse()
expect(envs).toHaveLength(1)
expect(envs[0].machine_name).toBe('mac1')
expect(envs[0].last_poll_at).toBeGreaterThan(0)
})
})
describe("listActiveEnvironmentsByUsername", () => {
test("filters by username", () => {
registerEnvironment({ username: "alice" });
registerEnvironment({ username: "bob" });
expect(listActiveEnvironmentsByUsername("alice")).toHaveLength(1);
});
});
describe('listActiveEnvironmentsByUsername', () => {
test('filters by username', () => {
registerEnvironment({ username: 'alice' })
registerEnvironment({ username: 'bob' })
expect(listActiveEnvironmentsByUsername('alice')).toHaveLength(1)
})
})
describe("reconnectEnvironment", () => {
test("sets status back to active", () => {
const result = registerEnvironment({});
deregisterEnvironment(result.environment_id);
expect(getEnvironment(result.environment_id)?.status).toBe("deregistered");
reconnectEnvironment(result.environment_id);
expect(getEnvironment(result.environment_id)?.status).toBe("active");
});
});
});
describe('reconnectEnvironment', () => {
test('sets status back to active', () => {
const result = registerEnvironment({})
deregisterEnvironment(result.environment_id)
expect(getEnvironment(result.environment_id)?.status).toBe('deregistered')
reconnectEnvironment(result.environment_id)
expect(getEnvironment(result.environment_id)?.status).toBe('active')
})
})
})
// ---------- Transport Service ----------
describe("Transport Service", () => {
describe('Transport Service', () => {
beforeEach(() => {
storeReset();
storeReset()
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
removeEventBus(key)
}
});
})
describe("normalizePayload", () => {
test("handles string payload", () => {
const result = normalizePayload("user", "hello world");
expect(result.content).toBe("hello world");
expect(result.raw).toBe("hello world");
});
describe('normalizePayload', () => {
test('handles string payload', () => {
const result = normalizePayload('user', 'hello world')
expect(result.content).toBe('hello world')
expect(result.raw).toBe('hello world')
})
test("handles null payload", () => {
const result = normalizePayload("user", null);
expect(result.content).toBe("");
expect(result.raw).toBeNull();
});
test('handles null payload', () => {
const result = normalizePayload('user', null)
expect(result.content).toBe('')
expect(result.raw).toBeNull()
})
test("handles object with direct content", () => {
const result = normalizePayload("user", { content: "direct text" });
expect(result.content).toBe("direct text");
});
test('handles object with direct content', () => {
const result = normalizePayload('user', { content: 'direct text' })
expect(result.content).toBe('direct text')
})
test("handles object with message.content string", () => {
const result = normalizePayload("assistant", { message: { role: "assistant", content: "reply" } });
expect(result.content).toBe("reply");
});
test('handles object with message.content string', () => {
const result = normalizePayload('assistant', {
message: { role: 'assistant', content: 'reply' },
})
expect(result.content).toBe('reply')
})
test("handles object with message.content array", () => {
const result = normalizePayload("assistant", {
test('handles object with message.content array', () => {
const result = normalizePayload('assistant', {
message: {
content: [
{ type: "text", text: "hello " },
{ type: "text", text: "world" },
{ type: 'text', text: 'hello ' },
{ type: 'text', text: 'world' },
],
},
});
expect(result.content).toBe("hello world");
});
})
expect(result.content).toBe('hello world')
})
test("preserves tool fields", () => {
const result = normalizePayload("tool_use", { tool_name: "Bash", tool_input: { cmd: "ls" } });
expect(result.tool_name).toBe("Bash");
expect(result.tool_input).toEqual({ cmd: "ls" });
});
test('preserves tool fields', () => {
const result = normalizePayload('tool_use', {
tool_name: 'Bash',
tool_input: { cmd: 'ls' },
})
expect(result.tool_name).toBe('Bash')
expect(result.tool_input).toEqual({ cmd: 'ls' })
})
test("preserves permission fields", () => {
const result = normalizePayload("permission", {
request_id: "req_1",
test('preserves permission fields', () => {
const result = normalizePayload('permission', {
request_id: 'req_1',
approved: true,
updated_input: { cmd: "ls -la" },
});
expect(result.request_id).toBe("req_1");
expect(result.approved).toBe(true);
expect(result.updated_input).toEqual({ cmd: "ls -la" });
});
updated_input: { cmd: 'ls -la' },
})
expect(result.request_id).toBe('req_1')
expect(result.approved).toBe(true)
expect(result.updated_input).toEqual({ cmd: 'ls -la' })
})
test("preserves message field", () => {
const msg = { role: "user", content: "hi" };
const result = normalizePayload("user", { message: msg });
expect(result.message).toEqual(msg);
});
test('preserves message field', () => {
const msg = { role: 'user', content: 'hi' }
const result = normalizePayload('user', { message: msg })
expect(result.message).toEqual(msg)
})
test("preserves uuid field", () => {
const result = normalizePayload("user", {
uuid: "msg_123",
content: "hi",
});
expect(result.uuid).toBe("msg_123");
});
test('preserves uuid field', () => {
const result = normalizePayload('user', {
uuid: 'msg_123',
content: 'hi',
})
expect(result.uuid).toBe('msg_123')
})
test("preserves isSynthetic field", () => {
const result = normalizePayload("user", {
content: "scheduled job: refresh analytics cache",
test('preserves isSynthetic field', () => {
const result = normalizePayload('user', {
content: 'scheduled job: refresh analytics cache',
isSynthetic: true,
});
expect(result.isSynthetic).toBe(true);
});
})
expect(result.isSynthetic).toBe(true)
})
test("uses name as tool_name fallback", () => {
const result = normalizePayload("tool", { name: "Read" });
expect(result.tool_name).toBe("Read");
});
test('uses name as tool_name fallback', () => {
const result = normalizePayload('tool', { name: 'Read' })
expect(result.tool_name).toBe('Read')
})
test("uses input as tool_input fallback", () => {
const result = normalizePayload("tool", { input: { path: "/tmp" } });
expect(result.tool_input).toEqual({ path: "/tmp" });
});
test('uses input as tool_input fallback', () => {
const result = normalizePayload('tool', { input: { path: '/tmp' } })
expect(result.tool_input).toEqual({ path: '/tmp' })
})
test("handles empty content array", () => {
const result = normalizePayload("assistant", {
test('handles empty content array', () => {
const result = normalizePayload('assistant', {
message: { content: [] },
});
expect(result.content).toBe("");
});
})
expect(result.content).toBe('')
})
test("preserves task_state fields", () => {
const result = normalizePayload("task_state", {
task_list_id: "team-alpha",
tasks: [{ id: "1", subject: "Task 1", status: "pending" }],
});
expect(result.task_list_id).toBe("team-alpha");
test('preserves task_state fields', () => {
const result = normalizePayload('task_state', {
task_list_id: 'team-alpha',
tasks: [{ id: '1', subject: 'Task 1', status: 'pending' }],
})
expect(result.task_list_id).toBe('team-alpha')
expect(result.tasks).toEqual([
{ id: "1", subject: "Task 1", status: "pending" },
]);
});
{ id: '1', subject: 'Task 1', status: 'pending' },
])
})
test("preserves status metadata for conversation reset events", () => {
const result = normalizePayload("status", {
status: "conversation_cleared",
subtype: "status",
message: "conversation_cleared",
});
expect(result.status).toBe("conversation_cleared");
expect(result.subtype).toBe("status");
expect(result.message).toBe("conversation_cleared");
});
test('preserves status metadata for conversation reset events', () => {
const result = normalizePayload('status', {
status: 'conversation_cleared',
subtype: 'status',
message: 'conversation_cleared',
})
expect(result.status).toBe('conversation_cleared')
expect(result.subtype).toBe('status')
expect(result.message).toBe('conversation_cleared')
})
test("handles undefined payload", () => {
const result = normalizePayload("user", undefined);
expect(result.content).toBe("");
});
});
test('handles undefined payload', () => {
const result = normalizePayload('user', undefined)
expect(result.content).toBe('')
})
})
describe("publishSessionEvent", () => {
test("publishes event to session bus", () => {
const event = publishSessionEvent("s1", "user", { content: "hello" }, "outbound");
expect(event.type).toBe("user");
expect(event.direction).toBe("outbound");
expect(event.sessionId).toBe("s1");
expect(event.seqNum).toBe(1);
});
describe('publishSessionEvent', () => {
test('publishes event to session bus', () => {
const event = publishSessionEvent(
's1',
'user',
{ content: 'hello' },
'outbound',
)
expect(event.type).toBe('user')
expect(event.direction).toBe('outbound')
expect(event.sessionId).toBe('s1')
expect(event.seqNum).toBe(1)
})
test("normalizes payload before publishing", () => {
const event = publishSessionEvent("s1", "assistant", { message: { content: "reply" } }, "inbound");
const payload = event.payload as Record<string, unknown>;
expect(payload.content).toBe("reply");
});
});
});
test('normalizes payload before publishing', () => {
const event = publishSessionEvent(
's1',
'assistant',
{ message: { content: 'reply' } },
'inbound',
)
const payload = event.payload as Record<string, unknown>
expect(payload.content).toBe('reply')
})
})
})

View File

@@ -1,11 +1,11 @@
import { describe, test, expect, beforeEach, mock } from "bun:test";
import { describe, test, expect, beforeEach, mock } from 'bun:test'
// Mock config
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
host: '0.0.0.0',
apiKeys: ['test-api-key'],
baseUrl: 'http://localhost:3000',
pollTimeout: 8,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
@@ -13,170 +13,201 @@ const mockConfig = {
webCorsOrigins: [],
wsIdleTimeout: 30,
wsKeepaliveInterval: 20,
};
mock.module("../config", () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
import { Hono } from "hono";
import { storeReset } from "../store";
import { removeEventBus, getAllEventBuses, getEventBus } from "../transport/event-bus";
import { createSSEWriter, createSSEStream } from "../transport/sse-writer";
/** Read up to N bytes from a Response stream, then cancel */
async function readPartialStream(res: Response, maxBytes = 4096): Promise<string> {
const reader = res.body?.getReader();
if (!reader) return "";
const chunks: Uint8Array[] = [];
let totalBytes = 0;
try {
while (totalBytes < maxBytes) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalBytes += value.length;
// Cancel after we have some data (first keepalive + any initial events)
if (totalBytes > 0) break;
}
} finally {
reader.cancel();
}
const combined = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(combined);
}
describe("SSE Writer", () => {
describe("createSSEWriter", () => {
test("creates SSEWriter with send and close methods", () => {
const app = new Hono();
let capturedWriter: ReturnType<typeof createSSEWriter> | null = null;
mock.module('../config', () => ({
config: mockConfig,
getBaseUrl: () => 'http://localhost:3000',
}))
app.get("/test", (c) => {
capturedWriter = createSSEWriter(c);
return c.text("ok");
});
import { Hono } from 'hono'
import { storeReset } from '../store'
import {
removeEventBus,
getAllEventBuses,
getEventBus,
} from '../transport/event-bus'
import { createSSEWriter, createSSEStream } from '../transport/sse-writer'
app.request("/test");
expect(capturedWriter).not.toBeNull();
expect(typeof capturedWriter!.send).toBe("function");
expect(typeof capturedWriter!.close).toBe("function");
});
});
/** Read up to N bytes from a Response stream, then cancel */
async function readPartialStream(
res: Response,
maxBytes = 4096,
): Promise<string> {
const reader = res.body?.getReader()
if (!reader) return ''
const chunks: Uint8Array[] = []
let totalBytes = 0
try {
while (totalBytes < maxBytes) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
totalBytes += value.length
// Cancel after we have some data (first keepalive + any initial events)
if (totalBytes > 0) break
}
} finally {
reader.cancel()
}
const combined = new Uint8Array(totalBytes)
let offset = 0
for (const chunk of chunks) {
combined.set(chunk, offset)
offset += chunk.length
}
return new TextDecoder().decode(combined)
}
describe("createSSEStream", () => {
describe('SSE Writer', () => {
describe('createSSEWriter', () => {
test('creates SSEWriter with send and close methods', () => {
const app = new Hono()
let capturedWriter: ReturnType<typeof createSSEWriter> | null = null
app.get('/test', c => {
capturedWriter = createSSEWriter(c)
return c.text('ok')
})
app.request('/test')
expect(capturedWriter).not.toBeNull()
expect(typeof capturedWriter!.send).toBe('function')
expect(typeof capturedWriter!.close).toBe('function')
})
})
describe('createSSEStream', () => {
beforeEach(() => {
storeReset();
storeReset()
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
removeEventBus(key)
}
});
})
test("returns Response with correct SSE headers", async () => {
const app = new Hono();
test('returns Response with correct SSE headers', async () => {
const app = new Hono()
app.get("/stream/:sessionId", (c) => {
const sessionId = c.req.param("sessionId");
return createSSEStream(c, sessionId, 0);
});
app.get('/stream/:sessionId', c => {
const sessionId = c.req.param('sessionId')
return createSSEStream(c, sessionId, 0)
})
const res = await app.request("/stream/s1");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toBe("text/event-stream");
expect(res.headers.get("Cache-Control")).toBe("no-cache");
expect(res.headers.get("Connection")).toBe("keep-alive");
expect(res.headers.get("X-Accel-Buffering")).toBe("no");
const res = await app.request('/stream/s1')
expect(res.status).toBe(200)
expect(res.headers.get('Content-Type')).toBe('text/event-stream')
expect(res.headers.get('Cache-Control')).toBe('no-cache')
expect(res.headers.get('Connection')).toBe('keep-alive')
expect(res.headers.get('X-Accel-Buffering')).toBe('no')
// Cancel the stream
res.body?.cancel();
});
res.body?.cancel()
})
test("sends initial keepalive", async () => {
const app = new Hono();
test('sends initial keepalive', async () => {
const app = new Hono()
app.get("/stream/:sessionId", (c) => {
const sessionId = c.req.param("sessionId");
return createSSEStream(c, sessionId, 0);
});
app.get('/stream/:sessionId', c => {
const sessionId = c.req.param('sessionId')
return createSSEStream(c, sessionId, 0)
})
const res = await app.request("/stream/s2");
const text = await readPartialStream(res);
expect(text).toContain(": keepalive");
});
const res = await app.request('/stream/s2')
const text = await readPartialStream(res)
expect(text).toContain(': keepalive')
})
test("sends historical events when fromSeqNum > 0", async () => {
test('sends historical events when fromSeqNum > 0', async () => {
// Pre-populate event bus with events
const bus = getEventBus("s3");
bus.publish({ id: "e1", sessionId: "s3", type: "user", payload: { content: "hello" }, direction: "outbound" });
bus.publish({ id: "e2", sessionId: "s3", type: "assistant", payload: { content: "hi" }, direction: "inbound" });
const bus = getEventBus('s3')
bus.publish({
id: 'e1',
sessionId: 's3',
type: 'user',
payload: { content: 'hello' },
direction: 'outbound',
})
bus.publish({
id: 'e2',
sessionId: 's3',
type: 'assistant',
payload: { content: 'hi' },
direction: 'inbound',
})
const app = new Hono();
const app = new Hono()
app.get("/stream/:sessionId", (c) => {
const sessionId = c.req.param("sessionId");
const fromSeq = parseInt(c.req.query("fromSeq") || "0");
return createSSEStream(c, sessionId, fromSeq);
});
app.get('/stream/:sessionId', c => {
const sessionId = c.req.param('sessionId')
const fromSeq = parseInt(c.req.query('fromSeq') || '0', 10)
return createSSEStream(c, sessionId, fromSeq)
})
const res = await app.request("/stream/s3?fromSeq=1");
const text = await readPartialStream(res);
const res = await app.request('/stream/s3?fromSeq=1')
const text = await readPartialStream(res)
// Should replay events since seq 1 (i.e., event 2)
expect(text).toContain('"seqNum":2');
expect(text).toContain("assistant");
});
expect(text).toContain('"seqNum":2')
expect(text).toContain('assistant')
})
test("no historical events when fromSeqNum is 0", async () => {
const bus = getEventBus("s5");
bus.publish({ id: "e1", sessionId: "s5", type: "user", payload: {}, direction: "outbound" });
test('no historical events when fromSeqNum is 0', async () => {
const bus = getEventBus('s5')
bus.publish({
id: 'e1',
sessionId: 's5',
type: 'user',
payload: {},
direction: 'outbound',
})
const app = new Hono();
const app = new Hono()
app.get("/stream/:sessionId", (c) => {
const sessionId = c.req.param("sessionId");
return createSSEStream(c, sessionId, 0);
});
app.get('/stream/:sessionId', c => {
const sessionId = c.req.param('sessionId')
return createSSEStream(c, sessionId, 0)
})
const res = await app.request("/stream/s5");
const text = await readPartialStream(res);
const res = await app.request('/stream/s5')
const text = await readPartialStream(res)
// With fromSeqNum=0, no historical replay, just keepalive
expect(text).toContain(": keepalive");
expect(text).toContain(': keepalive')
// Should NOT contain event data (only keepalive)
expect(text).not.toContain("event: message");
});
expect(text).not.toContain('event: message')
})
test("subscribes to new events and delivers them", async () => {
const app = new Hono();
test('subscribes to new events and delivers them', async () => {
const app = new Hono()
app.get("/stream/:sessionId", (c) => {
const sessionId = c.req.param("sessionId");
return createSSEStream(c, sessionId, 0);
});
app.get('/stream/:sessionId', c => {
const sessionId = c.req.param('sessionId')
return createSSEStream(c, sessionId, 0)
})
const res = await app.request("/stream/s6");
const res = await app.request('/stream/s6')
// Read initial keepalive first
const reader = res.body!.getReader();
const { value: firstChunk } = await reader.read();
const initialText = new TextDecoder().decode(firstChunk!);
expect(initialText).toContain(": keepalive");
const reader = res.body!.getReader()
const { value: firstChunk } = await reader.read()
const initialText = new TextDecoder().decode(firstChunk!)
expect(initialText).toContain(': keepalive')
// Now publish an event
const bus = getEventBus("s6");
bus.publish({ id: "e1", sessionId: "s6", type: "user", payload: { content: "real-time" }, direction: "outbound" });
const bus = getEventBus('s6')
bus.publish({
id: 'e1',
sessionId: 's6',
type: 'user',
payload: { content: 'real-time' },
direction: 'outbound',
})
// Read the event
const { value: secondChunk } = await reader.read();
const eventText = new TextDecoder().decode(secondChunk!);
expect(eventText).toContain("event: message");
expect(eventText).toContain("real-time");
const { value: secondChunk } = await reader.read()
const eventText = new TextDecoder().decode(secondChunk!)
expect(eventText).toContain('event: message')
expect(eventText).toContain('real-time')
reader.cancel();
});
});
});
reader.cancel()
})
})
})

View File

@@ -1,4 +1,4 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { describe, test, expect, beforeEach } from 'bun:test'
import {
storeReset,
storeCreateUser,
@@ -25,372 +25,401 @@ import {
storeGetWorkItem,
storeGetPendingWorkItem,
storeUpdateWorkItem,
} from "../store";
} from '../store'
describe("store", () => {
describe('store', () => {
beforeEach(() => {
storeReset();
});
storeReset()
})
// ---------- User ----------
describe("storeCreateUser", () => {
test("creates a new user", () => {
const user = storeCreateUser("alice");
expect(user.username).toBe("alice");
expect(user.createdAt).toBeInstanceOf(Date);
});
describe('storeCreateUser', () => {
test('creates a new user', () => {
const user = storeCreateUser('alice')
expect(user.username).toBe('alice')
expect(user.createdAt).toBeInstanceOf(Date)
})
test("returns existing user on duplicate create", () => {
const first = storeCreateUser("bob");
const second = storeCreateUser("bob");
expect(first).toBe(second);
});
});
test('returns existing user on duplicate create', () => {
const first = storeCreateUser('bob')
const second = storeCreateUser('bob')
expect(first).toBe(second)
})
})
describe("storeGetUser", () => {
test("returns undefined for non-existent user", () => {
expect(storeGetUser("nobody")).toBeUndefined();
});
describe('storeGetUser', () => {
test('returns undefined for non-existent user', () => {
expect(storeGetUser('nobody')).toBeUndefined()
})
test("returns created user", () => {
storeCreateUser("charlie");
const user = storeGetUser("charlie");
expect(user?.username).toBe("charlie");
});
});
test('returns created user', () => {
storeCreateUser('charlie')
const user = storeGetUser('charlie')
expect(user?.username).toBe('charlie')
})
})
// ---------- Token ----------
describe("storeCreateToken / storeGetUserByToken", () => {
test("creates and resolves token", () => {
storeCreateUser("dave");
storeCreateToken("dave", "tk_123");
const entry = storeGetUserByToken("tk_123");
expect(entry?.username).toBe("dave");
expect(entry?.createdAt).toBeInstanceOf(Date);
});
describe('storeCreateToken / storeGetUserByToken', () => {
test('creates and resolves token', () => {
storeCreateUser('dave')
storeCreateToken('dave', 'tk_123')
const entry = storeGetUserByToken('tk_123')
expect(entry?.username).toBe('dave')
expect(entry?.createdAt).toBeInstanceOf(Date)
})
test("returns undefined for unknown token", () => {
expect(storeGetUserByToken("nonexistent")).toBeUndefined();
});
});
test('returns undefined for unknown token', () => {
expect(storeGetUserByToken('nonexistent')).toBeUndefined()
})
})
describe("storeDeleteToken", () => {
test("deletes an existing token", () => {
storeCreateUser("eve");
storeCreateToken("eve", "tk_del");
expect(storeDeleteToken("tk_del")).toBe(true);
expect(storeGetUserByToken("tk_del")).toBeUndefined();
});
describe('storeDeleteToken', () => {
test('deletes an existing token', () => {
storeCreateUser('eve')
storeCreateToken('eve', 'tk_del')
expect(storeDeleteToken('tk_del')).toBe(true)
expect(storeGetUserByToken('tk_del')).toBeUndefined()
})
test("returns false for non-existent token", () => {
expect(storeDeleteToken("nope")).toBe(false);
});
});
test('returns false for non-existent token', () => {
expect(storeDeleteToken('nope')).toBe(false)
})
})
// ---------- Environment ----------
describe("storeCreateEnvironment", () => {
test("creates environment with defaults", () => {
const env = storeCreateEnvironment({ secret: "s1" });
expect(env.id).toMatch(/^env_/);
expect(env.secret).toBe("s1");
expect(env.status).toBe("active");
expect(env.machineName).toBeNull();
expect(env.maxSessions).toBe(1);
expect(env.workerType).toBe("claude_code");
expect(env.lastPollAt).toBeInstanceOf(Date);
});
describe('storeCreateEnvironment', () => {
test('creates environment with defaults', () => {
const env = storeCreateEnvironment({ secret: 's1' })
expect(env.id).toMatch(/^env_/)
expect(env.secret).toBe('s1')
expect(env.status).toBe('active')
expect(env.machineName).toBeNull()
expect(env.maxSessions).toBe(1)
expect(env.workerType).toBe('claude_code')
expect(env.lastPollAt).toBeInstanceOf(Date)
})
test("creates environment with all options", () => {
test('creates environment with all options', () => {
const env = storeCreateEnvironment({
secret: "s2",
machineName: "mac1",
directory: "/home/user",
branch: "main",
gitRepoUrl: "https://github.com/test/repo",
secret: 's2',
machineName: 'mac1',
directory: '/home/user',
branch: 'main',
gitRepoUrl: 'https://github.com/test/repo',
maxSessions: 5,
workerType: "custom",
bridgeId: "bridge1",
username: "alice",
});
expect(env.machineName).toBe("mac1");
expect(env.directory).toBe("/home/user");
expect(env.branch).toBe("main");
expect(env.gitRepoUrl).toBe("https://github.com/test/repo");
expect(env.maxSessions).toBe(5);
expect(env.workerType).toBe("custom");
expect(env.bridgeId).toBe("bridge1");
expect(env.username).toBe("alice");
});
});
workerType: 'custom',
bridgeId: 'bridge1',
username: 'alice',
})
expect(env.machineName).toBe('mac1')
expect(env.directory).toBe('/home/user')
expect(env.branch).toBe('main')
expect(env.gitRepoUrl).toBe('https://github.com/test/repo')
expect(env.maxSessions).toBe(5)
expect(env.workerType).toBe('custom')
expect(env.bridgeId).toBe('bridge1')
expect(env.username).toBe('alice')
})
})
describe("storeGetEnvironment", () => {
test("returns undefined for non-existent env", () => {
expect(storeGetEnvironment("env_no")).toBeUndefined();
});
describe('storeGetEnvironment', () => {
test('returns undefined for non-existent env', () => {
expect(storeGetEnvironment('env_no')).toBeUndefined()
})
test("returns created environment", () => {
const env = storeCreateEnvironment({ secret: "s" });
expect(storeGetEnvironment(env.id)).toBe(env);
});
});
test('returns created environment', () => {
const env = storeCreateEnvironment({ secret: 's' })
expect(storeGetEnvironment(env.id)).toBe(env)
})
})
describe("storeUpdateEnvironment", () => {
test("updates existing environment", () => {
const env = storeCreateEnvironment({ secret: "s" });
const result = storeUpdateEnvironment(env.id, { status: "disconnected" });
expect(result).toBe(true);
const updated = storeGetEnvironment(env.id);
expect(updated?.status).toBe("disconnected");
expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(env.updatedAt.getTime());
});
describe('storeUpdateEnvironment', () => {
test('updates existing environment', () => {
const env = storeCreateEnvironment({ secret: 's' })
const result = storeUpdateEnvironment(env.id, { status: 'disconnected' })
expect(result).toBe(true)
const updated = storeGetEnvironment(env.id)
expect(updated?.status).toBe('disconnected')
expect(updated?.updatedAt.getTime()).toBeGreaterThanOrEqual(
env.updatedAt.getTime(),
)
})
test("returns false for non-existent environment", () => {
expect(storeUpdateEnvironment("env_no", { status: "active" })).toBe(false);
});
});
test('returns false for non-existent environment', () => {
expect(storeUpdateEnvironment('env_no', { status: 'active' })).toBe(false)
})
})
describe("storeListActiveEnvironments", () => {
test("returns only active environments", () => {
const env1 = storeCreateEnvironment({ secret: "s1" });
const env2 = storeCreateEnvironment({ secret: "s2" });
storeUpdateEnvironment(env1.id, { status: "deregistered" });
const active = storeListActiveEnvironments();
expect(active).toHaveLength(1);
expect(active[0].id).toBe(env2.id);
});
});
describe('storeListActiveEnvironments', () => {
test('returns only active environments', () => {
const env1 = storeCreateEnvironment({ secret: 's1' })
const env2 = storeCreateEnvironment({ secret: 's2' })
storeUpdateEnvironment(env1.id, { status: 'deregistered' })
const active = storeListActiveEnvironments()
expect(active).toHaveLength(1)
expect(active[0].id).toBe(env2.id)
})
})
describe("storeListActiveEnvironmentsByUsername", () => {
test("filters by username", () => {
storeCreateEnvironment({ secret: "s1", username: "alice" });
storeCreateEnvironment({ secret: "s2", username: "bob" });
const aliceEnvs = storeListActiveEnvironmentsByUsername("alice");
expect(aliceEnvs).toHaveLength(1);
expect(aliceEnvs[0].username).toBe("alice");
});
});
describe('storeListActiveEnvironmentsByUsername', () => {
test('filters by username', () => {
storeCreateEnvironment({ secret: 's1', username: 'alice' })
storeCreateEnvironment({ secret: 's2', username: 'bob' })
const aliceEnvs = storeListActiveEnvironmentsByUsername('alice')
expect(aliceEnvs).toHaveLength(1)
expect(aliceEnvs[0].username).toBe('alice')
})
})
// ---------- Session ----------
describe("storeCreateSession", () => {
test("creates session with defaults", () => {
const session = storeCreateSession({});
expect(session.id).toMatch(/^session_/);
expect(session.status).toBe("idle");
expect(session.source).toBe("remote-control");
expect(session.environmentId).toBeNull();
expect(session.workerEpoch).toBe(0);
});
describe('storeCreateSession', () => {
test('creates session with defaults', () => {
const session = storeCreateSession({})
expect(session.id).toMatch(/^session_/)
expect(session.status).toBe('idle')
expect(session.source).toBe('remote-control')
expect(session.environmentId).toBeNull()
expect(session.workerEpoch).toBe(0)
})
test("creates session with options", () => {
const env = storeCreateEnvironment({ secret: "s" });
test('creates session with options', () => {
const env = storeCreateEnvironment({ secret: 's' })
const session = storeCreateSession({
environmentId: env.id,
title: "Test Session",
source: "cli",
permissionMode: "auto",
username: "alice",
});
expect(session.environmentId).toBe(env.id);
expect(session.title).toBe("Test Session");
expect(session.source).toBe("cli");
expect(session.permissionMode).toBe("auto");
expect(session.username).toBe("alice");
});
title: 'Test Session',
source: 'cli',
permissionMode: 'auto',
username: 'alice',
})
expect(session.environmentId).toBe(env.id)
expect(session.title).toBe('Test Session')
expect(session.source).toBe('cli')
expect(session.permissionMode).toBe('auto')
expect(session.username).toBe('alice')
})
test("creates session with custom idPrefix", () => {
const session = storeCreateSession({ idPrefix: "cse_" });
expect(session.id).toMatch(/^cse_/);
});
});
test('creates session with custom idPrefix', () => {
const session = storeCreateSession({ idPrefix: 'cse_' })
expect(session.id).toMatch(/^cse_/)
})
})
describe("storeGetSession", () => {
test("returns undefined for non-existent session", () => {
expect(storeGetSession("nope")).toBeUndefined();
});
});
describe('storeGetSession', () => {
test('returns undefined for non-existent session', () => {
expect(storeGetSession('nope')).toBeUndefined()
})
})
describe("storeUpdateSession", () => {
test("updates existing session", () => {
const session = storeCreateSession({});
const result = storeUpdateSession(session.id, { title: "Updated", status: "active" });
expect(result).toBe(true);
const updated = storeGetSession(session.id);
expect(updated?.title).toBe("Updated");
expect(updated?.status).toBe("active");
});
describe('storeUpdateSession', () => {
test('updates existing session', () => {
const session = storeCreateSession({})
const result = storeUpdateSession(session.id, {
title: 'Updated',
status: 'active',
})
expect(result).toBe(true)
const updated = storeGetSession(session.id)
expect(updated?.title).toBe('Updated')
expect(updated?.status).toBe('active')
})
test("returns false for non-existent session", () => {
expect(storeUpdateSession("nope", { title: "x" })).toBe(false);
});
test('returns false for non-existent session', () => {
expect(storeUpdateSession('nope', { title: 'x' })).toBe(false)
})
test("increments workerEpoch", () => {
const session = storeCreateSession({});
storeUpdateSession(session.id, { workerEpoch: 1 });
expect(storeGetSession(session.id)?.workerEpoch).toBe(1);
});
});
test('increments workerEpoch', () => {
const session = storeCreateSession({})
storeUpdateSession(session.id, { workerEpoch: 1 })
expect(storeGetSession(session.id)?.workerEpoch).toBe(1)
})
})
describe("storeListSessions", () => {
test("returns all sessions", () => {
storeCreateSession({});
storeCreateSession({});
expect(storeListSessions()).toHaveLength(2);
});
});
describe('storeListSessions', () => {
test('returns all sessions', () => {
storeCreateSession({})
storeCreateSession({})
expect(storeListSessions()).toHaveLength(2)
})
})
describe("storeListSessionsByUsername", () => {
test("filters by username", () => {
storeCreateSession({ username: "alice" });
storeCreateSession({ username: "bob" });
expect(storeListSessionsByUsername("alice")).toHaveLength(1);
});
});
describe('storeListSessionsByUsername', () => {
test('filters by username', () => {
storeCreateSession({ username: 'alice' })
storeCreateSession({ username: 'bob' })
expect(storeListSessionsByUsername('alice')).toHaveLength(1)
})
})
describe("storeListSessionsByEnvironment", () => {
test("filters by environment", () => {
const env = storeCreateEnvironment({ secret: "s" });
storeCreateSession({ environmentId: env.id });
storeCreateSession({});
expect(storeListSessionsByEnvironment(env.id)).toHaveLength(1);
});
});
describe('storeListSessionsByEnvironment', () => {
test('filters by environment', () => {
const env = storeCreateEnvironment({ secret: 's' })
storeCreateSession({ environmentId: env.id })
storeCreateSession({})
expect(storeListSessionsByEnvironment(env.id)).toHaveLength(1)
})
})
describe("storeDeleteSession", () => {
test("deletes existing session", () => {
const session = storeCreateSession({});
expect(storeDeleteSession(session.id)).toBe(true);
expect(storeGetSession(session.id)).toBeUndefined();
});
describe('storeDeleteSession', () => {
test('deletes existing session', () => {
const session = storeCreateSession({})
expect(storeDeleteSession(session.id)).toBe(true)
expect(storeGetSession(session.id)).toBeUndefined()
})
test("returns false for non-existent session", () => {
expect(storeDeleteSession("nope")).toBe(false);
});
});
test('returns false for non-existent session', () => {
expect(storeDeleteSession('nope')).toBe(false)
})
})
// ---------- Session Ownership ----------
describe("storeBindSession / storeIsSessionOwner", () => {
test("binds and checks ownership", () => {
const session = storeCreateSession({});
storeBindSession(session.id, "uuid-1");
expect(storeIsSessionOwner(session.id, "uuid-1")).toBe(true);
expect(storeIsSessionOwner(session.id, "uuid-2")).toBe(false);
});
describe('storeBindSession / storeIsSessionOwner', () => {
test('binds and checks ownership', () => {
const session = storeCreateSession({})
storeBindSession(session.id, 'uuid-1')
expect(storeIsSessionOwner(session.id, 'uuid-1')).toBe(true)
expect(storeIsSessionOwner(session.id, 'uuid-2')).toBe(false)
})
test("unbound session has no owner", () => {
const session = storeCreateSession({});
expect(storeIsSessionOwner(session.id, "uuid-1")).toBe(false);
});
test('unbound session has no owner', () => {
const session = storeCreateSession({})
expect(storeIsSessionOwner(session.id, 'uuid-1')).toBe(false)
})
test("multiple owners per session", () => {
const session = storeCreateSession({});
storeBindSession(session.id, "uuid-1");
storeBindSession(session.id, "uuid-2");
expect(storeIsSessionOwner(session.id, "uuid-1")).toBe(true);
expect(storeIsSessionOwner(session.id, "uuid-2")).toBe(true);
});
});
test('multiple owners per session', () => {
const session = storeCreateSession({})
storeBindSession(session.id, 'uuid-1')
storeBindSession(session.id, 'uuid-2')
expect(storeIsSessionOwner(session.id, 'uuid-1')).toBe(true)
expect(storeIsSessionOwner(session.id, 'uuid-2')).toBe(true)
})
})
describe("storeListSessionsByOwnerUuid", () => {
test("returns sessions owned by uuid", () => {
const s1 = storeCreateSession({});
const s2 = storeCreateSession({});
storeBindSession(s1.id, "uuid-1");
storeBindSession(s2.id, "uuid-1");
const owned = storeListSessionsByOwnerUuid("uuid-1");
expect(owned).toHaveLength(2);
});
describe('storeListSessionsByOwnerUuid', () => {
test('returns sessions owned by uuid', () => {
const s1 = storeCreateSession({})
const s2 = storeCreateSession({})
storeBindSession(s1.id, 'uuid-1')
storeBindSession(s2.id, 'uuid-1')
const owned = storeListSessionsByOwnerUuid('uuid-1')
expect(owned).toHaveLength(2)
})
test("returns empty for unknown uuid", () => {
expect(storeListSessionsByOwnerUuid("nope")).toHaveLength(0);
});
test('returns empty for unknown uuid', () => {
expect(storeListSessionsByOwnerUuid('nope')).toHaveLength(0)
})
test("excludes deleted sessions", () => {
const s1 = storeCreateSession({});
storeBindSession(s1.id, "uuid-1");
storeDeleteSession(s1.id);
expect(storeListSessionsByOwnerUuid("uuid-1")).toHaveLength(0);
});
});
test('excludes deleted sessions', () => {
const s1 = storeCreateSession({})
storeBindSession(s1.id, 'uuid-1')
storeDeleteSession(s1.id)
expect(storeListSessionsByOwnerUuid('uuid-1')).toHaveLength(0)
})
})
// ---------- Work Items ----------
describe("storeCreateWorkItem", () => {
test("creates work item with defaults", () => {
describe('storeCreateWorkItem', () => {
test('creates work item with defaults', () => {
const item = storeCreateWorkItem({
environmentId: "env1",
sessionId: "ses1",
secret: "sec1",
});
expect(item.id).toMatch(/^work_/);
expect(item.environmentId).toBe("env1");
expect(item.sessionId).toBe("ses1");
expect(item.state).toBe("pending");
expect(item.secret).toBe("sec1");
});
});
environmentId: 'env1',
sessionId: 'ses1',
secret: 'sec1',
})
expect(item.id).toMatch(/^work_/)
expect(item.environmentId).toBe('env1')
expect(item.sessionId).toBe('ses1')
expect(item.state).toBe('pending')
expect(item.secret).toBe('sec1')
})
})
describe("storeGetWorkItem", () => {
test("returns undefined for non-existent", () => {
expect(storeGetWorkItem("nope")).toBeUndefined();
});
describe('storeGetWorkItem', () => {
test('returns undefined for non-existent', () => {
expect(storeGetWorkItem('nope')).toBeUndefined()
})
test("returns created work item", () => {
const item = storeCreateWorkItem({ environmentId: "env1", sessionId: "ses1", secret: "s" });
expect(storeGetWorkItem(item.id)).toBe(item);
});
});
test('returns created work item', () => {
const item = storeCreateWorkItem({
environmentId: 'env1',
sessionId: 'ses1',
secret: 's',
})
expect(storeGetWorkItem(item.id)).toBe(item)
})
})
describe("storeGetPendingWorkItem", () => {
test("returns pending work for environment", () => {
const item = storeCreateWorkItem({ environmentId: "env1", sessionId: "ses1", secret: "s" });
const found = storeGetPendingWorkItem("env1");
expect(found?.id).toBe(item.id);
});
describe('storeGetPendingWorkItem', () => {
test('returns pending work for environment', () => {
const item = storeCreateWorkItem({
environmentId: 'env1',
sessionId: 'ses1',
secret: 's',
})
const found = storeGetPendingWorkItem('env1')
expect(found?.id).toBe(item.id)
})
test("returns undefined when no pending work", () => {
storeCreateWorkItem({ environmentId: "env1", sessionId: "ses1", secret: "s" });
expect(storeGetPendingWorkItem("env2")).toBeUndefined();
});
test('returns undefined when no pending work', () => {
storeCreateWorkItem({
environmentId: 'env1',
sessionId: 'ses1',
secret: 's',
})
expect(storeGetPendingWorkItem('env2')).toBeUndefined()
})
test("skips non-pending items", () => {
const item = storeCreateWorkItem({ environmentId: "env1", sessionId: "ses1", secret: "s" });
storeUpdateWorkItem(item.id, { state: "dispatched" });
expect(storeGetPendingWorkItem("env1")).toBeUndefined();
});
});
test('skips non-pending items', () => {
const item = storeCreateWorkItem({
environmentId: 'env1',
sessionId: 'ses1',
secret: 's',
})
storeUpdateWorkItem(item.id, { state: 'dispatched' })
expect(storeGetPendingWorkItem('env1')).toBeUndefined()
})
})
describe("storeUpdateWorkItem", () => {
test("updates existing work item", () => {
const item = storeCreateWorkItem({ environmentId: "env1", sessionId: "ses1", secret: "s" });
expect(storeUpdateWorkItem(item.id, { state: "acked" })).toBe(true);
expect(storeGetWorkItem(item.id)?.state).toBe("acked");
});
describe('storeUpdateWorkItem', () => {
test('updates existing work item', () => {
const item = storeCreateWorkItem({
environmentId: 'env1',
sessionId: 'ses1',
secret: 's',
})
expect(storeUpdateWorkItem(item.id, { state: 'acked' })).toBe(true)
expect(storeGetWorkItem(item.id)?.state).toBe('acked')
})
test("returns false for non-existent", () => {
expect(storeUpdateWorkItem("nope", { state: "acked" })).toBe(false);
});
});
test('returns false for non-existent', () => {
expect(storeUpdateWorkItem('nope', { state: 'acked' })).toBe(false)
})
})
// ---------- storeReset ----------
describe("storeReset", () => {
test("clears all data", () => {
storeCreateUser("alice");
storeCreateEnvironment({ secret: "s" });
storeCreateSession({});
storeCreateWorkItem({ environmentId: "env1", sessionId: "ses1", secret: "s" });
describe('storeReset', () => {
test('clears all data', () => {
storeCreateUser('alice')
storeCreateEnvironment({ secret: 's' })
storeCreateSession({})
storeCreateWorkItem({
environmentId: 'env1',
sessionId: 'ses1',
secret: 's',
})
storeReset();
storeReset()
expect(storeGetUser("alice")).toBeUndefined();
expect(storeListActiveEnvironments()).toHaveLength(0);
expect(storeListSessions()).toHaveLength(0);
expect(storeGetPendingWorkItem("env1")).toBeUndefined();
});
});
});
expect(storeGetUser('alice')).toBeUndefined()
expect(storeListActiveEnvironments()).toHaveLength(0)
expect(storeListSessions()).toHaveLength(0)
expect(storeGetPendingWorkItem('env1')).toBeUndefined()
})
})
})

View File

@@ -1,6 +1,6 @@
import { describe, test, expect } from "bun:test";
import { describe, test, expect } from 'bun:test'
const { normalizePayload } = await import("../services/transport");
const { normalizePayload } = await import('../services/transport')
// extractContent is not exported; we test it via normalizePayload's content field
@@ -8,181 +8,190 @@ const { normalizePayload } = await import("../services/transport");
// extractContent (via normalizePayload content field)
// =============================================================================
describe("extractContent", () => {
test("returns empty string for null payload", () => {
const result = normalizePayload("assistant", null);
expect(result.content).toBe("");
});
describe('extractContent', () => {
test('returns empty string for null payload', () => {
const result = normalizePayload('assistant', null)
expect(result.content).toBe('')
})
test("returns empty string for undefined payload", () => {
const result = normalizePayload("assistant", undefined);
expect(result.content).toBe("");
});
test('returns empty string for undefined payload', () => {
const result = normalizePayload('assistant', undefined)
expect(result.content).toBe('')
})
test("returns the string for string payload", () => {
const result = normalizePayload("assistant", "hello world");
expect(result.content).toBe("hello world");
});
test('returns the string for string payload', () => {
const result = normalizePayload('assistant', 'hello world')
expect(result.content).toBe('hello world')
})
test("extracts content field from object payload", () => {
const result = normalizePayload("assistant", { content: "direct content" });
expect(result.content).toBe("direct content");
});
test('extracts content field from object payload', () => {
const result = normalizePayload('assistant', { content: 'direct content' })
expect(result.content).toBe('direct content')
})
test("extracts message.content string from object payload", () => {
const result = normalizePayload("assistant", { message: { content: "msg content" } });
expect(result.content).toBe("msg content");
});
test('extracts message.content string from object payload', () => {
const result = normalizePayload('assistant', {
message: { content: 'msg content' },
})
expect(result.content).toBe('msg content')
})
test("extracts text blocks from message.content array", () => {
test('extracts text blocks from message.content array', () => {
const payload = {
message: {
content: [
{ type: "text", text: "Hello " },
{ type: "text", text: "World" },
{ type: 'text', text: 'Hello ' },
{ type: 'text', text: 'World' },
],
},
};
const result = normalizePayload("assistant", payload);
expect(result.content).toBe("Hello World");
});
}
const result = normalizePayload('assistant', payload)
expect(result.content).toBe('Hello World')
})
test("ignores non-text blocks in message.content array", () => {
test('ignores non-text blocks in message.content array', () => {
const payload = {
message: {
content: [
{ type: "image", url: "http://example.com/img.png" },
{ type: "text", text: "only this" },
{ type: 'image', url: 'http://example.com/img.png' },
{ type: 'text', text: 'only this' },
],
},
};
const result = normalizePayload("assistant", payload);
expect(result.content).toBe("only this");
});
}
const result = normalizePayload('assistant', payload)
expect(result.content).toBe('only this')
})
test("returns empty string when no extractable content", () => {
const result = normalizePayload("assistant", { foo: "bar" });
expect(result.content).toBe("");
});
test('returns empty string when no extractable content', () => {
const result = normalizePayload('assistant', { foo: 'bar' })
expect(result.content).toBe('')
})
test("prefers direct content over message.content", () => {
const result = normalizePayload("assistant", { content: "direct", message: { content: "nested" } });
expect(result.content).toBe("direct");
});
});
test('prefers direct content over message.content', () => {
const result = normalizePayload('assistant', {
content: 'direct',
message: { content: 'nested' },
})
expect(result.content).toBe('direct')
})
})
// =============================================================================
// normalizePayload — field preservation
// =============================================================================
describe("normalizePayload — field preservation", () => {
test("preserves raw payload", () => {
const payload = { content: "test", extra: true };
const result = normalizePayload("assistant", payload);
expect(result.raw).toBe(payload);
});
describe('normalizePayload — field preservation', () => {
test('preserves raw payload', () => {
const payload = { content: 'test', extra: true }
const result = normalizePayload('assistant', payload)
expect(result.raw).toBe(payload)
})
test("preserves uuid field", () => {
const result = normalizePayload("assistant", { uuid: "u-123" });
expect(result.uuid).toBe("u-123");
});
test('preserves uuid field', () => {
const result = normalizePayload('assistant', { uuid: 'u-123' })
expect(result.uuid).toBe('u-123')
})
test("does not preserve uuid when empty string", () => {
const result = normalizePayload("assistant", { uuid: "" });
expect(result.uuid).toBeUndefined();
});
test('does not preserve uuid when empty string', () => {
const result = normalizePayload('assistant', { uuid: '' })
expect(result.uuid).toBeUndefined()
})
test("preserves isSynthetic boolean", () => {
const result = normalizePayload("assistant", { isSynthetic: true });
expect(result.isSynthetic).toBe(true);
});
test('preserves isSynthetic boolean', () => {
const result = normalizePayload('assistant', { isSynthetic: true })
expect(result.isSynthetic).toBe(true)
})
test("preserves status string", () => {
const result = normalizePayload("assistant", { status: "running" });
expect(result.status).toBe("running");
});
test('preserves status string', () => {
const result = normalizePayload('assistant', { status: 'running' })
expect(result.status).toBe('running')
})
test("preserves subtype string", () => {
const result = normalizePayload("assistant", { subtype: "progress" });
expect(result.subtype).toBe("progress");
});
test('preserves subtype string', () => {
const result = normalizePayload('assistant', { subtype: 'progress' })
expect(result.subtype).toBe('progress')
})
test("preserves tool_name from tool_name field", () => {
const result = normalizePayload("tool", { tool_name: "bash" });
expect(result.tool_name).toBe("bash");
});
test('preserves tool_name from tool_name field', () => {
const result = normalizePayload('tool', { tool_name: 'bash' })
expect(result.tool_name).toBe('bash')
})
test("preserves tool_name from name field", () => {
const result = normalizePayload("tool", { name: "read" });
expect(result.tool_name).toBe("read");
});
test('preserves tool_name from name field', () => {
const result = normalizePayload('tool', { name: 'read' })
expect(result.tool_name).toBe('read')
})
test("preserves tool_input from tool_input field", () => {
const input = { command: "ls" };
const result = normalizePayload("tool", { tool_input: input });
expect(result.tool_input).toEqual(input);
});
test('preserves tool_input from tool_input field', () => {
const input = { command: 'ls' }
const result = normalizePayload('tool', { tool_input: input })
expect(result.tool_input).toEqual(input)
})
test("preserves tool_input from input field", () => {
const input = { path: "/tmp" };
const result = normalizePayload("tool", { input });
expect(result.tool_input).toEqual(input);
});
test('preserves tool_input from input field', () => {
const input = { path: '/tmp' }
const result = normalizePayload('tool', { input })
expect(result.tool_input).toEqual(input)
})
test("preserves request_id", () => {
const result = normalizePayload("permission", { request_id: "req-1" });
expect(result.request_id).toBe("req-1");
});
test('preserves request_id', () => {
const result = normalizePayload('permission', { request_id: 'req-1' })
expect(result.request_id).toBe('req-1')
})
test("preserves request object", () => {
const req = { subtype: "permission" };
const result = normalizePayload("permission", { request: req });
expect(result.request).toEqual(req);
});
test('preserves request object', () => {
const req = { subtype: 'permission' }
const result = normalizePayload('permission', { request: req })
expect(result.request).toEqual(req)
})
test("preserves approved field", () => {
const result = normalizePayload("permission", { approved: true });
expect(result.approved).toBe(true);
});
test('preserves approved field', () => {
const result = normalizePayload('permission', { approved: true })
expect(result.approved).toBe(true)
})
test("preserves updated_input", () => {
const input = { command: "rm -rf" };
const result = normalizePayload("permission", { updated_input: input });
expect(result.updated_input).toEqual(input);
});
test('preserves updated_input', () => {
const input = { command: 'rm -rf' }
const result = normalizePayload('permission', { updated_input: input })
expect(result.updated_input).toEqual(input)
})
test("preserves message field for backward compat", () => {
const msg = { role: "user", content: "hi" };
const result = normalizePayload("assistant", { message: msg });
expect(result.message).toEqual(msg);
});
});
test('preserves message field for backward compat', () => {
const msg = { role: 'user', content: 'hi' }
const result = normalizePayload('assistant', { message: msg })
expect(result.message).toEqual(msg)
})
})
// =============================================================================
// normalizePayload — task_state special handling
// =============================================================================
describe("normalizePayload — task_state type", () => {
test("preserves task_list_id (snake_case)", () => {
const result = normalizePayload("task_state", { task_list_id: "tl-1" });
expect(result.task_list_id).toBe("tl-1");
});
describe('normalizePayload — task_state type', () => {
test('preserves task_list_id (snake_case)', () => {
const result = normalizePayload('task_state', { task_list_id: 'tl-1' })
expect(result.task_list_id).toBe('tl-1')
})
test("preserves taskListId (camelCase)", () => {
const result = normalizePayload("task_state", { taskListId: "tl-2" });
expect(result.taskListId).toBe("tl-2");
});
test('preserves taskListId (camelCase)', () => {
const result = normalizePayload('task_state', { taskListId: 'tl-2' })
expect(result.taskListId).toBe('tl-2')
})
test("preserves tasks array", () => {
const tasks = [{ id: "t1", title: "Task 1" }];
const result = normalizePayload("task_state", { tasks });
expect(result.tasks).toEqual(tasks);
});
test('preserves tasks array', () => {
const tasks = [{ id: 't1', title: 'Task 1' }]
const result = normalizePayload('task_state', { tasks })
expect(result.tasks).toEqual(tasks)
})
test("does not preserve task fields for non-task_state type", () => {
const result = normalizePayload("assistant", { task_list_id: "tl-1", taskListId: "tl-2", tasks: [] });
expect(result.task_list_id).toBeUndefined();
expect(result.taskListId).toBeUndefined();
expect(result.tasks).toBeUndefined();
});
});
test('does not preserve task fields for non-task_state type', () => {
const result = normalizePayload('assistant', {
task_list_id: 'tl-1',
taskListId: 'tl-2',
tasks: [],
})
expect(result.task_list_id).toBeUndefined()
expect(result.taskListId).toBeUndefined()
expect(result.tasks).toBeUndefined()
})
})

View File

@@ -1,11 +1,11 @@
import { describe, test, expect, beforeEach, mock } from "bun:test";
import { describe, test, expect, beforeEach, mock } from 'bun:test'
// Mock config before imports
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
host: '0.0.0.0',
apiKeys: ['test-api-key'],
baseUrl: 'http://localhost:3000',
pollTimeout: 1, // Short timeout for tests
heartbeatInterval: 20,
jwtExpiresIn: 3600,
@@ -13,14 +13,20 @@ const mockConfig = {
webCorsOrigins: [],
wsIdleTimeout: 30,
wsKeepaliveInterval: 20,
};
}
mock.module("../config", () => ({
mock.module('../config', () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
getBaseUrl: () => 'http://localhost:3000',
}))
import { storeReset, storeCreateEnvironment, storeCreateSession, storeGetWorkItem, storeGetPendingWorkItem } from "../store";
import {
storeReset,
storeCreateEnvironment,
storeCreateSession,
storeGetWorkItem,
storeGetPendingWorkItem,
} from '../store'
import {
createWorkItem,
pollWork,
@@ -28,132 +34,138 @@ import {
stopWork,
heartbeatWork,
reconnectWorkForEnvironment,
} from "../services/work-dispatch";
} from '../services/work-dispatch'
describe("Work Dispatch", () => {
let envId: string;
let sessionId: string;
describe('Work Dispatch', () => {
let envId: string
let sessionId: string
beforeEach(() => {
storeReset();
const env = storeCreateEnvironment({ secret: "s" });
envId = env.id;
const session = storeCreateSession({ environmentId: envId });
sessionId = session.id;
});
storeReset()
const env = storeCreateEnvironment({ secret: 's' })
envId = env.id
const session = storeCreateSession({ environmentId: envId })
sessionId = session.id
})
describe("createWorkItem", () => {
test("creates work item for active environment", async () => {
const workId = await createWorkItem(envId, sessionId);
expect(workId).toMatch(/^work_/);
const item = storeGetWorkItem(workId);
expect(item?.state).toBe("pending");
expect(item?.sessionId).toBe(sessionId);
});
describe('createWorkItem', () => {
test('creates work item for active environment', async () => {
const workId = await createWorkItem(envId, sessionId)
expect(workId).toMatch(/^work_/)
const item = storeGetWorkItem(workId)
expect(item?.state).toBe('pending')
expect(item?.sessionId).toBe(sessionId)
})
test("throws for non-existent environment", async () => {
await expect(createWorkItem("env_no", sessionId)).rejects.toThrow("not found");
});
test('throws for non-existent environment', async () => {
await expect(createWorkItem('env_no', sessionId)).rejects.toThrow(
'not found',
)
})
test("throws for inactive environment", async () => {
const inactiveEnv = storeCreateEnvironment({ secret: "s2" });
test('throws for inactive environment', async () => {
const inactiveEnv = storeCreateEnvironment({ secret: 's2' })
// Manually set status to deregistered
const { storeUpdateEnvironment } = await import("../store");
storeUpdateEnvironment(inactiveEnv.id, { status: "deregistered" });
await expect(createWorkItem(inactiveEnv.id, sessionId)).rejects.toThrow("not active");
});
const { storeUpdateEnvironment } = await import('../store')
storeUpdateEnvironment(inactiveEnv.id, { status: 'deregistered' })
await expect(createWorkItem(inactiveEnv.id, sessionId)).rejects.toThrow(
'not active',
)
})
test("encodes work secret as base64 JSON", async () => {
const workId = await createWorkItem(envId, sessionId);
const item = storeGetWorkItem(workId);
const decoded = JSON.parse(Buffer.from(item!.secret, "base64url").toString());
expect(decoded.version).toBe(1);
expect(decoded.session_ingress_token).toBe("test-api-key");
expect(decoded.api_base_url).toBe("http://localhost:3000");
});
});
test('encodes work secret as base64 JSON', async () => {
const workId = await createWorkItem(envId, sessionId)
const item = storeGetWorkItem(workId)
const decoded = JSON.parse(
Buffer.from(item!.secret, 'base64url').toString(),
)
expect(decoded.version).toBe(1)
expect(decoded.session_ingress_token).toBe('test-api-key')
expect(decoded.api_base_url).toBe('http://localhost:3000')
})
})
describe("pollWork", () => {
test("returns null when no work available (timeout)", async () => {
const result = await pollWork(envId, 0.1);
expect(result).toBeNull();
});
describe('pollWork', () => {
test('returns null when no work available (timeout)', async () => {
const result = await pollWork(envId, 0.1)
expect(result).toBeNull()
})
test("returns pending work and marks as dispatched", async () => {
const workId = await createWorkItem(envId, sessionId);
const result = await pollWork(envId, 1);
expect(result).not.toBeNull();
expect(result!.id).toBe(workId);
expect(result!.state).toBe("dispatched");
expect(result!.data.type).toBe("session");
expect(result!.data.id).toBe(sessionId);
test('returns pending work and marks as dispatched', async () => {
const workId = await createWorkItem(envId, sessionId)
const result = await pollWork(envId, 1)
expect(result).not.toBeNull()
expect(result!.id).toBe(workId)
expect(result!.state).toBe('dispatched')
expect(result!.data.type).toBe('session')
expect(result!.data.id).toBe(sessionId)
// Work should no longer be pending
expect(storeGetPendingWorkItem(envId)).toBeUndefined();
});
expect(storeGetPendingWorkItem(envId)).toBeUndefined()
})
test("does not return work for different environment", async () => {
const env2 = storeCreateEnvironment({ secret: "s2" });
await createWorkItem(envId, sessionId);
const result = await pollWork(env2.id, 0.1);
expect(result).toBeNull();
});
});
test('does not return work for different environment', async () => {
const env2 = storeCreateEnvironment({ secret: 's2' })
await createWorkItem(envId, sessionId)
const result = await pollWork(env2.id, 0.1)
expect(result).toBeNull()
})
})
describe("ackWork", () => {
test("marks work as acked", async () => {
const workId = await createWorkItem(envId, sessionId);
ackWork(workId);
expect(storeGetWorkItem(workId)?.state).toBe("acked");
});
});
describe('ackWork', () => {
test('marks work as acked', async () => {
const workId = await createWorkItem(envId, sessionId)
ackWork(workId)
expect(storeGetWorkItem(workId)?.state).toBe('acked')
})
})
describe("stopWork", () => {
test("marks work as completed", async () => {
const workId = await createWorkItem(envId, sessionId);
stopWork(workId);
expect(storeGetWorkItem(workId)?.state).toBe("completed");
});
});
describe('stopWork', () => {
test('marks work as completed', async () => {
const workId = await createWorkItem(envId, sessionId)
stopWork(workId)
expect(storeGetWorkItem(workId)?.state).toBe('completed')
})
})
describe("heartbeatWork", () => {
test("extends lease and returns heartbeat info", async () => {
const workId = await createWorkItem(envId, sessionId);
const result = heartbeatWork(workId);
expect(result.lease_extended).toBe(true);
expect(result.ttl_seconds).toBe(40); // heartbeatInterval * 2
expect(result.last_heartbeat).toBeTruthy();
});
describe('heartbeatWork', () => {
test('extends lease and returns heartbeat info', async () => {
const workId = await createWorkItem(envId, sessionId)
const result = heartbeatWork(workId)
expect(result.lease_extended).toBe(true)
expect(result.ttl_seconds).toBe(40) // heartbeatInterval * 2
expect(result.last_heartbeat).toBeTruthy()
})
test("returns default state for non-existent work", async () => {
const result = heartbeatWork("work_no");
expect(result.state).toBe("acked");
});
});
test('returns default state for non-existent work', async () => {
const result = heartbeatWork('work_no')
expect(result.state).toBe('acked')
})
})
describe("reconnectWorkForEnvironment", () => {
test("creates work items for idle sessions in environment", async () => {
describe('reconnectWorkForEnvironment', () => {
test('creates work items for idle sessions in environment', async () => {
// Create another idle session
storeCreateSession({ environmentId: envId });
const workIds = await reconnectWorkForEnvironment(envId);
expect(workIds).toHaveLength(2);
storeCreateSession({ environmentId: envId })
const workIds = await reconnectWorkForEnvironment(envId)
expect(workIds).toHaveLength(2)
for (const id of workIds) {
expect(storeGetWorkItem(id)?.state).toBe("pending");
expect(storeGetWorkItem(id)?.state).toBe('pending')
}
});
})
test("skips non-idle sessions", async () => {
const activeSession = storeCreateSession({ environmentId: envId });
const { storeUpdateSession } = await import("../store");
storeUpdateSession(activeSession.id, { status: "active" });
const workIds = await reconnectWorkForEnvironment(envId);
test('skips non-idle sessions', async () => {
const activeSession = storeCreateSession({ environmentId: envId })
const { storeUpdateSession } = await import('../store')
storeUpdateSession(activeSession.id, { status: 'active' })
const workIds = await reconnectWorkForEnvironment(envId)
// Only the original idle session should get work
expect(workIds).toHaveLength(1);
});
expect(workIds).toHaveLength(1)
})
test("returns empty for environment with no sessions", async () => {
const emptyEnv = storeCreateEnvironment({ secret: "s_empty" });
const workIds = await reconnectWorkForEnvironment(emptyEnv.id);
expect(workIds).toHaveLength(0);
});
});
});
test('returns empty for environment with no sessions', async () => {
const emptyEnv = storeCreateEnvironment({ secret: 's_empty' })
const workIds = await reconnectWorkForEnvironment(emptyEnv.id)
expect(workIds).toHaveLength(0)
})
})
})