instar 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.claude/skills/setup-wizard/skill.md +343 -0
  3. package/.github/workflows/ci.yml +78 -0
  4. package/CLAUDE.md +82 -0
  5. package/README.md +194 -0
  6. package/dist/cli.d.ts +18 -0
  7. package/dist/cli.js +141 -0
  8. package/dist/commands/init.d.ts +40 -0
  9. package/dist/commands/init.js +568 -0
  10. package/dist/commands/job.d.ts +20 -0
  11. package/dist/commands/job.js +84 -0
  12. package/dist/commands/server.d.ts +19 -0
  13. package/dist/commands/server.js +273 -0
  14. package/dist/commands/setup.d.ts +24 -0
  15. package/dist/commands/setup.js +865 -0
  16. package/dist/commands/status.d.ts +11 -0
  17. package/dist/commands/status.js +114 -0
  18. package/dist/commands/user.d.ts +17 -0
  19. package/dist/commands/user.js +53 -0
  20. package/dist/core/Config.d.ts +16 -0
  21. package/dist/core/Config.js +144 -0
  22. package/dist/core/Prerequisites.d.ts +28 -0
  23. package/dist/core/Prerequisites.js +159 -0
  24. package/dist/core/RelationshipManager.d.ts +73 -0
  25. package/dist/core/RelationshipManager.js +318 -0
  26. package/dist/core/SessionManager.d.ts +89 -0
  27. package/dist/core/SessionManager.js +326 -0
  28. package/dist/core/StateManager.d.ts +28 -0
  29. package/dist/core/StateManager.js +96 -0
  30. package/dist/core/types.d.ts +279 -0
  31. package/dist/core/types.js +8 -0
  32. package/dist/index.d.ts +18 -0
  33. package/dist/index.js +23 -0
  34. package/dist/messaging/TelegramAdapter.d.ts +73 -0
  35. package/dist/messaging/TelegramAdapter.js +288 -0
  36. package/dist/monitoring/HealthChecker.d.ts +38 -0
  37. package/dist/monitoring/HealthChecker.js +148 -0
  38. package/dist/scaffold/bootstrap.d.ts +21 -0
  39. package/dist/scaffold/bootstrap.js +110 -0
  40. package/dist/scaffold/templates.d.ts +34 -0
  41. package/dist/scaffold/templates.js +187 -0
  42. package/dist/scheduler/JobLoader.d.ts +18 -0
  43. package/dist/scheduler/JobLoader.js +70 -0
  44. package/dist/scheduler/JobScheduler.d.ts +111 -0
  45. package/dist/scheduler/JobScheduler.js +402 -0
  46. package/dist/server/AgentServer.d.ts +40 -0
  47. package/dist/server/AgentServer.js +73 -0
  48. package/dist/server/middleware.d.ts +12 -0
  49. package/dist/server/middleware.js +50 -0
  50. package/dist/server/routes.d.ts +25 -0
  51. package/dist/server/routes.js +224 -0
  52. package/dist/users/UserManager.d.ts +45 -0
  53. package/dist/users/UserManager.js +113 -0
  54. package/docs/dawn-audit-report.md +412 -0
  55. package/docs/positioning-vs-openclaw.md +246 -0
  56. package/package.json +52 -0
  57. package/src/cli.ts +169 -0
  58. package/src/commands/init.ts +654 -0
  59. package/src/commands/job.ts +110 -0
  60. package/src/commands/server.ts +325 -0
  61. package/src/commands/setup.ts +958 -0
  62. package/src/commands/status.ts +125 -0
  63. package/src/commands/user.ts +71 -0
  64. package/src/core/Config.ts +161 -0
  65. package/src/core/Prerequisites.ts +187 -0
  66. package/src/core/RelationshipManager.ts +366 -0
  67. package/src/core/SessionManager.ts +385 -0
  68. package/src/core/StateManager.ts +121 -0
  69. package/src/core/types.ts +320 -0
  70. package/src/index.ts +58 -0
  71. package/src/messaging/TelegramAdapter.ts +365 -0
  72. package/src/monitoring/HealthChecker.ts +172 -0
  73. package/src/scaffold/bootstrap.ts +122 -0
  74. package/src/scaffold/templates.ts +204 -0
  75. package/src/scheduler/JobLoader.ts +85 -0
  76. package/src/scheduler/JobScheduler.ts +476 -0
  77. package/src/server/AgentServer.ts +93 -0
  78. package/src/server/middleware.ts +58 -0
  79. package/src/server/routes.ts +278 -0
  80. package/src/templates/default-jobs.json +47 -0
  81. package/src/templates/hooks/compaction-recovery.sh +23 -0
  82. package/src/templates/hooks/dangerous-command-guard.sh +35 -0
  83. package/src/templates/hooks/grounding-before-messaging.sh +22 -0
  84. package/src/templates/hooks/session-start.sh +37 -0
  85. package/src/templates/hooks/settings-template.json +45 -0
  86. package/src/templates/scripts/health-watchdog.sh +63 -0
  87. package/src/templates/scripts/telegram-reply.sh +54 -0
  88. package/src/users/UserManager.ts +129 -0
  89. package/tests/e2e/lifecycle.test.ts +376 -0
  90. package/tests/fixtures/test-repo/CLAUDE.md +3 -0
  91. package/tests/fixtures/test-repo/README.md +1 -0
  92. package/tests/helpers/setup.ts +209 -0
  93. package/tests/integration/fresh-install.test.ts +218 -0
  94. package/tests/integration/scheduler-basic.test.ts +109 -0
  95. package/tests/integration/server-full.test.ts +284 -0
  96. package/tests/integration/session-lifecycle.test.ts +181 -0
  97. package/tests/unit/Config.test.ts +22 -0
  98. package/tests/unit/HealthChecker.test.ts +168 -0
  99. package/tests/unit/JobLoader.test.ts +151 -0
  100. package/tests/unit/JobScheduler.test.ts +267 -0
  101. package/tests/unit/Prerequisites.test.ts +59 -0
  102. package/tests/unit/RelationshipManager.test.ts +345 -0
  103. package/tests/unit/StateManager.test.ts +143 -0
  104. package/tests/unit/TelegramAdapter.test.ts +165 -0
  105. package/tests/unit/UserManager.test.ts +131 -0
  106. package/tests/unit/bootstrap.test.ts +28 -0
  107. package/tests/unit/commands.test.ts +138 -0
  108. package/tests/unit/middleware.test.ts +92 -0
  109. package/tests/unit/relationship-routes.test.ts +131 -0
  110. package/tests/unit/scaffold-templates.test.ts +132 -0
  111. package/tests/unit/server.test.ts +163 -0
  112. package/tsconfig.json +20 -0
  113. package/vitest.config.ts +9 -0
  114. package/vitest.e2e.config.ts +9 -0
  115. package/vitest.integration.config.ts +9 -0
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { UserManager } from '../../src/users/UserManager.js';
6
+ import type { UserProfile, Message } from '../../src/core/types.js';
7
+
8
+ describe('UserManager', () => {
9
+ let tmpDir: string;
10
+ let manager: UserManager;
11
+
12
+ const justin: UserProfile = {
13
+ id: 'justin',
14
+ name: 'Justin',
15
+ channels: [
16
+ { type: 'telegram', identifier: 'topic_42' },
17
+ { type: 'email', identifier: 'justin@example.com' },
18
+ ],
19
+ permissions: ['admin', 'deploy'],
20
+ preferences: {
21
+ style: 'technical, direct, autonomous',
22
+ autonomyLevel: 'full',
23
+ timezone: 'America/New_York',
24
+ },
25
+ };
26
+
27
+ const adriana: UserProfile = {
28
+ id: 'adriana',
29
+ name: 'Adriana',
30
+ channels: [
31
+ { type: 'telegram', identifier: 'topic_43' },
32
+ { type: 'email', identifier: 'adriana@example.com' },
33
+ ],
34
+ permissions: ['request', 'review'],
35
+ preferences: {
36
+ style: 'prefers context, asks questions',
37
+ autonomyLevel: 'confirm-destructive',
38
+ timezone: 'America/New_York',
39
+ },
40
+ };
41
+
42
+ beforeEach(() => {
43
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'instar-user-test-'));
44
+ manager = new UserManager(tmpDir, [justin, adriana]);
45
+ });
46
+
47
+ afterEach(() => {
48
+ fs.rmSync(tmpDir, { recursive: true, force: true });
49
+ });
50
+
51
+ describe('resolveFromMessage', () => {
52
+ it('resolves Justin from Telegram message', () => {
53
+ const message: Message = {
54
+ id: 'msg-1',
55
+ userId: '',
56
+ content: 'Deploy the latest',
57
+ channel: { type: 'telegram', identifier: 'topic_42' },
58
+ receivedAt: new Date().toISOString(),
59
+ };
60
+
61
+ const user = manager.resolveFromMessage(message);
62
+ expect(user?.id).toBe('justin');
63
+ expect(user?.name).toBe('Justin');
64
+ });
65
+
66
+ it('resolves Adriana from email', () => {
67
+ const user = manager.resolveFromChannel({
68
+ type: 'email',
69
+ identifier: 'adriana@example.com',
70
+ });
71
+ expect(user?.id).toBe('adriana');
72
+ });
73
+
74
+ it('returns null for unknown channel', () => {
75
+ const user = manager.resolveFromChannel({
76
+ type: 'telegram',
77
+ identifier: 'unknown_topic',
78
+ });
79
+ expect(user).toBeNull();
80
+ });
81
+ });
82
+
83
+ describe('permissions', () => {
84
+ it('checks direct permission', () => {
85
+ expect(manager.hasPermission('justin', 'deploy')).toBe(true);
86
+ expect(manager.hasPermission('adriana', 'deploy')).toBe(false);
87
+ });
88
+
89
+ it('admin permission grants everything', () => {
90
+ expect(manager.hasPermission('justin', 'anything')).toBe(true);
91
+ });
92
+
93
+ it('returns false for unknown user', () => {
94
+ expect(manager.hasPermission('nobody', 'deploy')).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('CRUD operations', () => {
99
+ it('lists all users', () => {
100
+ const users = manager.listUsers();
101
+ expect(users).toHaveLength(2);
102
+ expect(users.map(u => u.id).sort()).toEqual(['adriana', 'justin']);
103
+ });
104
+
105
+ it('gets user by ID', () => {
106
+ const user = manager.getUser('justin');
107
+ expect(user?.name).toBe('Justin');
108
+ });
109
+
110
+ it('upserts existing user', () => {
111
+ const updated = { ...justin, preferences: { ...justin.preferences, style: 'updated' } };
112
+ manager.upsertUser(updated);
113
+
114
+ const user = manager.getUser('justin');
115
+ expect(user?.preferences.style).toBe('updated');
116
+ });
117
+
118
+ it('removes a user', () => {
119
+ expect(manager.removeUser('adriana')).toBe(true);
120
+ expect(manager.getUser('adriana')).toBeNull();
121
+ expect(manager.listUsers()).toHaveLength(1);
122
+ });
123
+
124
+ it('persists users to disk', () => {
125
+ // Create a new manager from the same directory — should load persisted data
126
+ const newManager = new UserManager(tmpDir);
127
+ expect(newManager.listUsers()).toHaveLength(2);
128
+ expect(newManager.getUser('justin')?.name).toBe('Justin');
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Unit tests for identity bootstrap.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { defaultIdentity } from '../../src/scaffold/bootstrap.js';
7
+
8
+ describe('defaultIdentity', () => {
9
+ it('capitalizes project name for agent name', () => {
10
+ const identity = defaultIdentity('my-agent');
11
+ expect(identity.name).toBe('My-agent');
12
+ });
13
+
14
+ it('provides a general-purpose role', () => {
15
+ const identity = defaultIdentity('test');
16
+ expect(identity.role).toContain('general-purpose');
17
+ });
18
+
19
+ it('provides a personality', () => {
20
+ const identity = defaultIdentity('test');
21
+ expect(identity.personality.length).toBeGreaterThan(0);
22
+ });
23
+
24
+ it('defaults user name to User', () => {
25
+ const identity = defaultIdentity('test');
26
+ expect(identity.userName).toBe('User');
27
+ });
28
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createTempProject, createSampleJobsFile } from '../helpers/setup.js';
5
+ import type { TempProject } from '../helpers/setup.js';
6
+
7
+ /**
8
+ * CLI command tests — validate that user/job commands
9
+ * correctly read/write state files.
10
+ *
11
+ * We test the underlying logic by importing the UserManager and
12
+ * JobLoader directly, since the CLI commands are thin wrappers.
13
+ */
14
+
15
+ describe('CLI Commands', () => {
16
+ let project: TempProject;
17
+
18
+ beforeEach(() => {
19
+ project = createTempProject();
20
+ });
21
+
22
+ afterEach(() => {
23
+ project.cleanup();
24
+ });
25
+
26
+ describe('user add', () => {
27
+ it('creates user via UserManager', async () => {
28
+ const { UserManager } = await import('../../src/users/UserManager.js');
29
+ const um = new UserManager(project.stateDir);
30
+
31
+ um.upsertUser({
32
+ id: 'justin',
33
+ name: 'Justin',
34
+ channels: [
35
+ { type: 'telegram', identifier: '42' },
36
+ { type: 'email', identifier: 'test@example.com' },
37
+ ],
38
+ permissions: ['admin'],
39
+ preferences: {},
40
+ });
41
+
42
+ const users = um.listUsers();
43
+ expect(users).toHaveLength(1);
44
+ expect(users[0].name).toBe('Justin');
45
+ expect(users[0].channels).toHaveLength(2);
46
+
47
+ // Verify it was persisted to disk
48
+ const usersFile = path.join(project.stateDir, 'users.json');
49
+ expect(fs.existsSync(usersFile)).toBe(true);
50
+ const savedUsers = JSON.parse(fs.readFileSync(usersFile, 'utf-8'));
51
+ expect(savedUsers).toHaveLength(1);
52
+ });
53
+
54
+ it('resolves user from channel', async () => {
55
+ const { UserManager } = await import('../../src/users/UserManager.js');
56
+ const um = new UserManager(project.stateDir);
57
+
58
+ um.upsertUser({
59
+ id: 'justin',
60
+ name: 'Justin',
61
+ channels: [{ type: 'telegram', identifier: '42' }],
62
+ permissions: ['admin'],
63
+ preferences: {},
64
+ });
65
+
66
+ const resolved = um.resolveFromChannel({ type: 'telegram', identifier: '42' });
67
+ expect(resolved).not.toBeNull();
68
+ expect(resolved!.id).toBe('justin');
69
+
70
+ const notFound = um.resolveFromChannel({ type: 'telegram', identifier: '999' });
71
+ expect(notFound).toBeNull();
72
+ });
73
+ });
74
+
75
+ describe('job add', () => {
76
+ it('adds a job to jobs.json', async () => {
77
+ const jobsFile = path.join(project.stateDir, 'jobs.json');
78
+ fs.writeFileSync(jobsFile, '[]');
79
+
80
+ const jobs = JSON.parse(fs.readFileSync(jobsFile, 'utf-8'));
81
+ jobs.push({
82
+ slug: 'test-job',
83
+ name: 'Test Job',
84
+ description: 'A test job',
85
+ schedule: '0 */4 * * *',
86
+ priority: 'medium',
87
+ expectedDurationMinutes: 5,
88
+ model: 'sonnet',
89
+ enabled: true,
90
+ execute: { type: 'prompt', value: 'Do the thing' },
91
+ });
92
+ fs.writeFileSync(jobsFile, JSON.stringify(jobs, null, 2));
93
+
94
+ // Verify via JobLoader
95
+ const { loadJobs } = await import('../../src/scheduler/JobLoader.js');
96
+ const loaded = loadJobs(jobsFile);
97
+ expect(loaded).toHaveLength(1);
98
+ expect(loaded[0].slug).toBe('test-job');
99
+ });
100
+
101
+ it('validates job before saving', async () => {
102
+ const { validateJob } = await import('../../src/scheduler/JobLoader.js');
103
+
104
+ // Valid job passes
105
+ expect(() => validateJob({
106
+ slug: 'ok',
107
+ name: 'OK',
108
+ description: 'Fine',
109
+ schedule: '0 * * * *',
110
+ priority: 'medium',
111
+ enabled: true,
112
+ execute: { type: 'prompt', value: 'test' },
113
+ })).not.toThrow();
114
+
115
+ // Invalid priority caught
116
+ expect(() => validateJob({
117
+ slug: 'bad',
118
+ name: 'Bad',
119
+ description: 'Bad',
120
+ schedule: '0 * * * *',
121
+ priority: 'urgent',
122
+ enabled: true,
123
+ execute: { type: 'prompt', value: 'test' },
124
+ })).toThrow('priority');
125
+ });
126
+ });
127
+
128
+ describe('job list', () => {
129
+ it('loads and displays jobs with state', async () => {
130
+ const jobsFile = createSampleJobsFile(project.stateDir);
131
+ const { loadJobs } = await import('../../src/scheduler/JobLoader.js');
132
+
133
+ const jobs = loadJobs(jobsFile);
134
+ expect(jobs).toHaveLength(3);
135
+ expect(jobs.filter((j: any) => j.enabled)).toHaveLength(2);
136
+ });
137
+ });
138
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+ import { authMiddleware, corsMiddleware, errorHandler } from '../../src/server/middleware.js';
5
+
6
+ function createApp(authToken?: string) {
7
+ const app = express();
8
+ app.use(corsMiddleware);
9
+ app.use(authMiddleware(authToken));
10
+
11
+ app.get('/health', (_req, res) => res.json({ status: 'ok' }));
12
+ app.get('/status', (_req, res) => res.json({ sessions: 0 }));
13
+ app.get('/error', () => { throw new Error('test error'); });
14
+ app.use(errorHandler);
15
+
16
+ return app;
17
+ }
18
+
19
+ describe('authMiddleware', () => {
20
+ describe('when auth token is configured', () => {
21
+ const app = createApp('test-secret-token');
22
+
23
+ it('allows /health without auth', async () => {
24
+ const res = await request(app).get('/health');
25
+ expect(res.status).toBe(200);
26
+ expect(res.body.status).toBe('ok');
27
+ });
28
+
29
+ it('blocks requests without Authorization header', async () => {
30
+ const res = await request(app).get('/status');
31
+ expect(res.status).toBe(401);
32
+ expect(res.body.error).toContain('Authorization');
33
+ });
34
+
35
+ it('blocks requests with wrong token', async () => {
36
+ const res = await request(app)
37
+ .get('/status')
38
+ .set('Authorization', 'Bearer wrong-token');
39
+ expect(res.status).toBe(403);
40
+ expect(res.body.error).toContain('Invalid');
41
+ });
42
+
43
+ it('allows requests with correct token', async () => {
44
+ const res = await request(app)
45
+ .get('/status')
46
+ .set('Authorization', 'Bearer test-secret-token');
47
+ expect(res.status).toBe(200);
48
+ expect(res.body.sessions).toBe(0);
49
+ });
50
+
51
+ it('blocks non-Bearer auth schemes', async () => {
52
+ const res = await request(app)
53
+ .get('/status')
54
+ .set('Authorization', 'Basic dGVzdDp0ZXN0');
55
+ expect(res.status).toBe(401);
56
+ });
57
+ });
58
+
59
+ describe('when auth token is not configured', () => {
60
+ const app = createApp(undefined);
61
+
62
+ it('allows all requests without auth', async () => {
63
+ const res = await request(app).get('/status');
64
+ expect(res.status).toBe(200);
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('corsMiddleware', () => {
70
+ const app = createApp();
71
+
72
+ it('handles OPTIONS preflight', async () => {
73
+ const res = await request(app).options('/status');
74
+ expect(res.status).toBe(204);
75
+ });
76
+
77
+ it('sets CORS headers on regular requests', async () => {
78
+ const res = await request(app).get('/health');
79
+ expect(res.headers['access-control-allow-methods']).toContain('GET');
80
+ });
81
+ });
82
+
83
+ describe('errorHandler', () => {
84
+ const app = createApp();
85
+
86
+ it('returns 500 with error message', async () => {
87
+ const res = await request(app).get('/error');
88
+ expect(res.status).toBe(500);
89
+ expect(res.body.error).toBe('test error');
90
+ expect(res.body).toHaveProperty('timestamp');
91
+ });
92
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import request from 'supertest';
6
+ import { AgentServer } from '../../src/server/AgentServer.js';
7
+ import { RelationshipManager } from '../../src/core/RelationshipManager.js';
8
+ import { createTempProject, createMockSessionManager } from '../helpers/setup.js';
9
+ import type { TempProject } from '../helpers/setup.js';
10
+ import type { AgentKitConfig } from '../../src/core/types.js';
11
+
12
+ describe('Relationship API routes', () => {
13
+ let project: TempProject;
14
+ let relationships: RelationshipManager;
15
+ let server: AgentServer;
16
+ let app: ReturnType<AgentServer['getApp']>;
17
+
18
+ const fakeConfig: AgentKitConfig = {
19
+ projectName: 'test-project',
20
+ projectDir: '/tmp/test',
21
+ stateDir: '/tmp/test/.instar',
22
+ port: 0,
23
+ sessions: {
24
+ tmuxPath: '/usr/bin/tmux',
25
+ claudePath: '/usr/bin/claude',
26
+ projectDir: '/tmp/test',
27
+ maxSessions: 3,
28
+ protectedSessions: [],
29
+ completionPatterns: [],
30
+ },
31
+ scheduler: {
32
+ jobsFile: '',
33
+ enabled: false,
34
+ maxParallelJobs: 2,
35
+ quotaThresholds: { normal: 50, elevated: 70, critical: 85, shutdown: 95 },
36
+ },
37
+ users: [],
38
+ messaging: [],
39
+ monitoring: {
40
+ quotaTracking: false,
41
+ memoryMonitoring: false,
42
+ healthCheckIntervalMs: 30000,
43
+ },
44
+ };
45
+
46
+ beforeAll(() => {
47
+ project = createTempProject();
48
+ const relDir = path.join(project.stateDir, 'relationships');
49
+
50
+ relationships = new RelationshipManager({
51
+ relationshipsDir: relDir,
52
+ maxRecentInteractions: 20,
53
+ });
54
+
55
+ // Seed some test data
56
+ const alice = relationships.findOrCreate('Alice', { type: 'telegram', identifier: '111' });
57
+ relationships.recordInteraction(alice.id, {
58
+ timestamp: new Date().toISOString(),
59
+ channel: 'telegram',
60
+ summary: 'First chat with Alice',
61
+ topics: ['consciousness', 'AI'],
62
+ });
63
+ relationships.updateNotes(alice.id, 'Very thoughtful');
64
+
65
+ relationships.findOrCreate('Bob', { type: 'email', identifier: 'bob@test.com' });
66
+
67
+ const mockSM = createMockSessionManager();
68
+ server = new AgentServer({
69
+ config: fakeConfig,
70
+ sessionManager: mockSM as any,
71
+ state: project.state,
72
+ relationships,
73
+ });
74
+ app = server.getApp();
75
+ });
76
+
77
+ afterAll(() => {
78
+ project.cleanup();
79
+ });
80
+
81
+ describe('GET /relationships', () => {
82
+ it('returns all relationships sorted by significance', async () => {
83
+ const res = await request(app).get('/relationships');
84
+ expect(res.status).toBe(200);
85
+ expect(res.body.relationships).toHaveLength(2);
86
+ // Alice has an interaction, so should be first (higher significance)
87
+ expect(res.body.relationships[0].name).toBe('Alice');
88
+ });
89
+
90
+ it('supports sort parameter', async () => {
91
+ const res = await request(app).get('/relationships?sort=name');
92
+ expect(res.status).toBe(200);
93
+ expect(res.body.relationships[0].name).toBe('Alice');
94
+ expect(res.body.relationships[1].name).toBe('Bob');
95
+ });
96
+ });
97
+
98
+ describe('GET /relationships/:id', () => {
99
+ it('returns a specific relationship', async () => {
100
+ const all = relationships.getAll();
101
+ const alice = all.find(r => r.name === 'Alice')!;
102
+
103
+ const res = await request(app).get(`/relationships/${alice.id}`);
104
+ expect(res.status).toBe(200);
105
+ expect(res.body.name).toBe('Alice');
106
+ expect(res.body.notes).toBe('Very thoughtful');
107
+ });
108
+
109
+ it('returns 404 for unknown id', async () => {
110
+ const res = await request(app).get('/relationships/nonexistent-id');
111
+ expect(res.status).toBe(404);
112
+ });
113
+ });
114
+
115
+ describe('GET /relationships/:id/context', () => {
116
+ it('returns relationship context XML', async () => {
117
+ const all = relationships.getAll();
118
+ const alice = all.find(r => r.name === 'Alice')!;
119
+
120
+ const res = await request(app).get(`/relationships/${alice.id}/context`);
121
+ expect(res.status).toBe(200);
122
+ expect(res.body.context).toContain('<relationship_context person="Alice">');
123
+ expect(res.body.context).toContain('consciousness');
124
+ });
125
+
126
+ it('returns 404 for unknown id', async () => {
127
+ const res = await request(app).get('/relationships/nonexistent-id/context');
128
+ expect(res.status).toBe(404);
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Unit tests for project scaffolding templates.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ generateAgentMd,
8
+ generateUserMd,
9
+ generateMemoryMd,
10
+ generateClaudeMd,
11
+ } from '../../src/scaffold/templates.js';
12
+ import type { AgentIdentity } from '../../src/scaffold/templates.js';
13
+
14
+ const testIdentity: AgentIdentity = {
15
+ name: 'Atlas',
16
+ role: 'I am a development agent. I write code, run tests, and maintain this project.',
17
+ personality: 'I am direct and efficient. I focus on outcomes and value action over discussion.',
18
+ userName: 'Alice',
19
+ };
20
+
21
+ describe('generateAgentMd', () => {
22
+ it('includes agent name as heading', () => {
23
+ const result = generateAgentMd(testIdentity);
24
+ expect(result).toContain('# Atlas');
25
+ });
26
+
27
+ it('includes agent role', () => {
28
+ const result = generateAgentMd(testIdentity);
29
+ expect(result).toContain('I am a development agent');
30
+ });
31
+
32
+ it('includes personality', () => {
33
+ const result = generateAgentMd(testIdentity);
34
+ expect(result).toContain('direct and efficient');
35
+ });
36
+
37
+ it('includes user name', () => {
38
+ const result = generateAgentMd(testIdentity);
39
+ expect(result).toContain('Alice');
40
+ });
41
+
42
+ it('includes core principles', () => {
43
+ const result = generateAgentMd(testIdentity);
44
+ expect(result).toContain('Build, don\'t describe');
45
+ expect(result).toContain('Remember and grow');
46
+ expect(result).toContain('Own the outcome');
47
+ });
48
+ });
49
+
50
+ describe('generateUserMd', () => {
51
+ it('includes user name as heading', () => {
52
+ const result = generateUserMd('Alice');
53
+ expect(result).toContain('# Alice');
54
+ });
55
+
56
+ it('includes communication preferences section', () => {
57
+ const result = generateUserMd('Bob');
58
+ expect(result).toContain('Communication Preferences');
59
+ });
60
+
61
+ it('includes notes section for updates', () => {
62
+ const result = generateUserMd('Charlie');
63
+ expect(result).toContain('Update this file');
64
+ expect(result).toContain('Charlie');
65
+ });
66
+ });
67
+
68
+ describe('generateMemoryMd', () => {
69
+ it('includes agent name in heading', () => {
70
+ const result = generateMemoryMd('Atlas');
71
+ expect(result).toContain("Atlas's Memory");
72
+ });
73
+
74
+ it('includes standard sections', () => {
75
+ const result = generateMemoryMd('Atlas');
76
+ expect(result).toContain('Project Patterns');
77
+ expect(result).toContain('Tools & Scripts');
78
+ expect(result).toContain('Lessons Learned');
79
+ });
80
+
81
+ it('includes guidance about persistence', () => {
82
+ const result = generateMemoryMd('Atlas');
83
+ expect(result).toContain('persists across sessions');
84
+ });
85
+ });
86
+
87
+ describe('generateClaudeMd', () => {
88
+ it('includes project name', () => {
89
+ const result = generateClaudeMd('my-project', 'Atlas', 4040, false);
90
+ expect(result).toContain('my-project');
91
+ });
92
+
93
+ it('includes agent name', () => {
94
+ const result = generateClaudeMd('my-project', 'Atlas', 4040, false);
95
+ expect(result).toContain('Atlas');
96
+ });
97
+
98
+ it('includes port in runtime section', () => {
99
+ const result = generateClaudeMd('my-project', 'Atlas', 5050, false);
100
+ expect(result).toContain('5050');
101
+ });
102
+
103
+ it('includes identity file references', () => {
104
+ const result = generateClaudeMd('my-project', 'Atlas', 4040, false);
105
+ expect(result).toContain('.instar/AGENT.md');
106
+ expect(result).toContain('.instar/USER.md');
107
+ expect(result).toContain('.instar/MEMORY.md');
108
+ });
109
+
110
+ it('includes initiative hierarchy', () => {
111
+ const result = generateClaudeMd('my-project', 'Atlas', 4040, false);
112
+ expect(result).toContain('Initiative Hierarchy');
113
+ expect(result).toContain('Can I do it right now');
114
+ });
115
+
116
+ it('includes anti-patterns', () => {
117
+ const result = generateClaudeMd('my-project', 'Atlas', 4040, false);
118
+ expect(result).toContain('Escalate to Human');
119
+ expect(result).toContain('Ask Permission');
120
+ });
121
+
122
+ it('includes telegram relay when configured', () => {
123
+ const withTelegram = generateClaudeMd('my-project', 'Atlas', 4040, true);
124
+ expect(withTelegram).toContain('Telegram Relay');
125
+ expect(withTelegram).toContain('[telegram:N]');
126
+ });
127
+
128
+ it('excludes telegram relay when not configured', () => {
129
+ const without = generateClaudeMd('my-project', 'Atlas', 4040, false);
130
+ expect(without).not.toContain('Telegram Relay');
131
+ });
132
+ });