Add installer, CI, ARM build and test updates

Introduce a one‑line Docker installer and environment examples, add CI and docker validation workflows, and update docs and tests.

Key changes:
- Add docker/install.sh installer that bootstraps /opt/synapsis, downloads compose files, optionally installs Docker, and generates secrets.
- Add top-level .env.example and docker/.env.example entries for shared storage and local development.
- Add GitHub Actions CI (ci.yml) with type checks, targeted vitest runs, build and docker-compose validation.
- Update existing docker workflow to set up QEMU and build multi‑arch images (amd64, arm64).
- Update README and docker/README to use the installer, point to the new repo, adjust local setup instructions, and note shared S3 env vars.
- Update docker-compose comments and include shared S3 env variables and defaults.
- Update .gitignore to ignore site-work mirrors.
- Tests: expand supported bot source types (add brave_news, youtube), increase POST_MAX_LENGTH from 400 to 600, add minimal user handle in mention handler tests, and add mocks & test adjustments in scheduler tests to keep them unit-scoped.

These changes simplify installation, add CI coverage (including Docker config validation), enable multi‑arch builds, and align tests with expanded bot source/validation behavior.
This commit is contained in:
cyph3rasi
2026-03-07 16:11:37 -08:00
parent a686db38b0
commit ae2c34df6c
14 changed files with 350 additions and 48 deletions
+4 -2
View File
@@ -750,11 +750,13 @@ describe('Feature: bot-system, Property 14: Multiple Source Types Per Bot', () =
);
});
it('SUPPORTED_SOURCE_TYPES contains all three types', () => {
it('SUPPORTED_SOURCE_TYPES contains the core and extended source types', () => {
expect(SUPPORTED_SOURCE_TYPES).toContain('rss');
expect(SUPPORTED_SOURCE_TYPES).toContain('reddit');
expect(SUPPORTED_SOURCE_TYPES).toContain('news_api');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(3);
expect(SUPPORTED_SOURCE_TYPES).toContain('brave_news');
expect(SUPPORTED_SOURCE_TYPES).toContain('youtube');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(5);
});
});
+3 -1
View File
@@ -433,7 +433,9 @@ describe('Constants', () => {
expect(SUPPORTED_SOURCE_TYPES).toContain('rss');
expect(SUPPORTED_SOURCE_TYPES).toContain('reddit');
expect(SUPPORTED_SOURCE_TYPES).toContain('news_api');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(4);
expect(SUPPORTED_SOURCE_TYPES).toContain('brave_news');
expect(SUPPORTED_SOURCE_TYPES).toContain('youtube');
expect(SUPPORTED_SOURCE_TYPES.length).toBe(5);
});
@@ -52,6 +52,9 @@ export const __addBot = (handle: string, userId: string) => {
id,
userId,
handle: handle.toLowerCase(),
user: {
handle: handle.toLowerCase(),
},
name: `Bot ${handle}`,
personalityConfig: JSON.stringify({
systemPrompt: 'Test bot',
+1 -1
View File
@@ -454,7 +454,7 @@ describe('Feature: bot-system, Property 38: Post Content Validation', () => {
it('validation constants are properly defined (Requirement 11.5)', async () => {
// Verify constants are reasonable
expect(POST_MAX_LENGTH).toBe(400);
expect(POST_MAX_LENGTH).toBe(600);
expect(POST_MIN_LENGTH).toBe(1);
expect(MAX_URLS_PER_POST).toBe(5);
+43 -1
View File
@@ -586,6 +586,22 @@ vi.mock('./rateLimiter', () => ({
canPost: vi.fn(async () => ({ allowed: true })),
}));
// Mock post creation so scheduler tests stay unit-scoped
vi.mock('./posting', () => ({
triggerPost: vi.fn(async () => ({
success: true,
post: {
id: 'post-1',
content: 'Generated post',
},
})),
}));
// Mock source fetching to avoid exercising fetcher behavior here
vi.mock('./contentFetcher', () => ({
fetchAllSourcesForBot: vi.fn(async () => undefined),
}));
import {
hasUnprocessedContent,
getNextUnprocessedContent,
@@ -742,7 +758,7 @@ describe('processScheduledPosts', () => {
expect(result.details[0].status).toBe('skipped_rate_limit');
});
it('should skip bots when no content available', async () => {
it('should post original content when a bot has no active sources', async () => {
const { db } = await import('@/db');
const { canPost } = await import('./rateLimiter');
@@ -760,6 +776,32 @@ describe('processScheduledPosts', () => {
vi.mocked(canPost).mockResolvedValue({ allowed: true });
vi.mocked(db.query.botContentSources.findMany).mockResolvedValue([]);
const result = await processScheduledPosts();
expect(result.processed).toBe(1);
expect(result.details[0].status).toBe('posted');
});
it('should skip bots when active sources have no content available', async () => {
const { db } = await import('@/db');
const { canPost } = await import('./rateLimiter');
vi.mocked(db.query.bots.findMany).mockResolvedValue([
{
id: 'bot-1',
scheduleConfig: JSON.stringify({
type: 'interval',
intervalMinutes: 30,
}),
lastPostAt: null,
} as any,
]);
vi.mocked(canPost).mockResolvedValue({ allowed: true });
vi.mocked(db.query.botContentSources.findMany).mockResolvedValue([
{ id: 'source-1' } as any,
]);
vi.mocked(db.query.botContentItems.findFirst).mockResolvedValue(undefined);
const result = await processScheduledPosts();
expect(result.skipped).toBe(1);
expect(result.details[0].status).toBe('skipped_no_content');