move from Types.ObjectId to GadgetId (a string)
This commit is contained in:
parent
50b9618d4e
commit
404532012e
@ -7,8 +7,9 @@ Fixed critical bugs in the Gadget Code socket messaging system that prevented me
|
|||||||
## Problems Identified
|
## Problems Identified
|
||||||
|
|
||||||
### 1. **Critical Bug: Socket Session Lookup Failure**
|
### 1. **Critical Bug: Socket Session Lookup Failure**
|
||||||
|
|
||||||
- **Location**: `gadget-code/src/services/socket.ts`
|
- **Location**: `gadget-code/src/services/socket.ts`
|
||||||
- **Issue**: `getDroneSession(registration)` looked up sessions using `registration._id.toHexString()` as the key, but sessions were stored with `socket.id` as the key
|
- **Issue**: `getDroneSession(registration)` looked up sessions using `registration._id` as the key, but sessions were stored with `socket.id` as the key
|
||||||
- **Impact**: ALL drone messaging was broken:
|
- **Impact**: ALL drone messaging was broken:
|
||||||
- `requestSessionLock` - couldn't lock drones
|
- `requestSessionLock` - couldn't lock drones
|
||||||
- `submitPrompt` - couldn't submit work orders
|
- `submitPrompt` - couldn't submit work orders
|
||||||
@ -16,11 +17,13 @@ Fixed critical bugs in the Gadget Code socket messaging system that prevented me
|
|||||||
- **Same issue existed for**: `getCodeSession(user)` looking up by `user._id` but storing by `socket.id`
|
- **Same issue existed for**: `getCodeSession(user)` looking up by `user._id` but storing by `socket.id`
|
||||||
|
|
||||||
### 2. **Missing requestTermination Handler**
|
### 2. **Missing requestTermination Handler**
|
||||||
|
|
||||||
- **Location**: `gadget-code/src/lib/drone-session.ts`
|
- **Location**: `gadget-code/src/lib/drone-session.ts`
|
||||||
- **Issue**: No handler registered for `requestTermination` event
|
- **Issue**: No handler registered for `requestTermination` event
|
||||||
- **Impact**: Even if session lookup worked, termination messages wouldn't be forwarded to drones
|
- **Impact**: Even if session lookup worked, termination messages wouldn't be forwarded to drones
|
||||||
|
|
||||||
### 3. **No Test Coverage**
|
### 3. **No Test Coverage**
|
||||||
|
|
||||||
- **Issue**: Zero tests for socket session management or termination flow
|
- **Issue**: Zero tests for socket session management or termination flow
|
||||||
- **Impact**: Bugs went undetected, no way to verify fixes
|
- **Impact**: Bugs went undetected, no way to verify fixes
|
||||||
|
|
||||||
@ -31,6 +34,7 @@ Fixed critical bugs in the Gadget Code socket messaging system that prevented me
|
|||||||
**File**: `gadget-code/src/services/socket.ts`
|
**File**: `gadget-code/src/services/socket.ts`
|
||||||
|
|
||||||
Added dual-index architecture:
|
Added dual-index architecture:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Primary storage by socket.id
|
// Primary storage by socket.id
|
||||||
private droneSessions: DroneSessionMap = new Map<string, DroneSession>();
|
private droneSessions: DroneSessionMap = new Map<string, DroneSession>();
|
||||||
@ -42,35 +46,38 @@ private codeSessionUserIndex: CodeSessionMap = new Map<string, CodeSession>();
|
|||||||
```
|
```
|
||||||
|
|
||||||
Updated `onSocketAuth()` to populate both indexes:
|
Updated `onSocketAuth()` to populate both indexes:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// For drones
|
// For drones
|
||||||
this.droneSessions.set(socket.id, droneSession);
|
this.droneSessions.set(socket.id, droneSession);
|
||||||
this.droneRegistrationIndex.set(registration._id.toHexString(), droneSession);
|
this.droneRegistrationIndex.set(registration._id, droneSession);
|
||||||
|
|
||||||
// For code/IDE sessions
|
// For code/IDE sessions
|
||||||
this.codeSessions.set(socket.id, session);
|
this.codeSessions.set(socket.id, session);
|
||||||
this.codeSessionUserIndex.set(user._id.toHexString(), session);
|
this.codeSessionUserIndex.set(user._id, session);
|
||||||
```
|
```
|
||||||
|
|
||||||
Updated `onSocketDisconnect()` to clean up both indexes:
|
Updated `onSocketDisconnect()` to clean up both indexes:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
case SocketSessionType.Drone:
|
case SocketSessionType.Drone:
|
||||||
const droneSession = this.droneSessions.get(socket.id);
|
const droneSession = this.droneSessions.get(socket.id);
|
||||||
if (droneSession) {
|
if (droneSession) {
|
||||||
this.droneRegistrationIndex.delete(droneSession.registration._id.toHexString());
|
this.droneRegistrationIndex.delete(droneSession.registration._id);
|
||||||
}
|
}
|
||||||
this.droneSessions.delete(socket.id);
|
this.droneSessions.delete(socket.id);
|
||||||
```
|
```
|
||||||
|
|
||||||
Updated lookup methods to use correct indexes:
|
Updated lookup methods to use correct indexes:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
getDroneSession(registration: IDroneRegistration): DroneSession {
|
getDroneSession(registration: IDroneRegistration): DroneSession {
|
||||||
const session = this.droneRegistrationIndex.get(registration._id.toHexString());
|
const session = this.droneRegistrationIndex.get(registration._id);
|
||||||
// ... error handling
|
// ... error handling
|
||||||
}
|
}
|
||||||
|
|
||||||
getCodeSession(ideSession: IIdeSession): CodeSession {
|
getCodeSession(ideSession: IIdeSession): CodeSession {
|
||||||
const session = this.codeSessionUserIndex.get(ideSession._id.toHexString());
|
const session = this.codeSessionUserIndex.get(ideSession._id);
|
||||||
// ... error handling
|
// ... error handling
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -80,6 +87,7 @@ getCodeSession(ideSession: IIdeSession): CodeSession {
|
|||||||
**File**: `gadget-code/src/lib/drone-session.ts`
|
**File**: `gadget-code/src/lib/drone-session.ts`
|
||||||
|
|
||||||
Added handler registration:
|
Added handler registration:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
register() {
|
register() {
|
||||||
super.register();
|
super.register();
|
||||||
@ -93,10 +101,11 @@ register() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Added handler implementation:
|
Added handler implementation:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
|
async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
|
||||||
this.log.info("requestTermination received, forwarding to drone", {
|
this.log.info("requestTermination received, forwarding to drone", {
|
||||||
registrationId: this.registration._id.toHexString(),
|
registrationId: this.registration._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.emit("requestTermination", (success: boolean) => {
|
this.socket.emit("requestTermination", (success: boolean) => {
|
||||||
@ -111,17 +120,20 @@ async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
|
|||||||
Created new test files and utilities:
|
Created new test files and utilities:
|
||||||
|
|
||||||
**Test Utilities**:
|
**Test Utilities**:
|
||||||
|
|
||||||
- `tests/helpers/socket-test-helpers.ts` - Mock factories and utilities
|
- `tests/helpers/socket-test-helpers.ts` - Mock factories and utilities
|
||||||
- `tests/fixtures/index.ts` - Export helpers for easy import
|
- `tests/fixtures/index.ts` - Export helpers for easy import
|
||||||
|
|
||||||
**New Test Files**:
|
**New Test Files**:
|
||||||
|
|
||||||
- `tests/socket-service.test.ts` - 12 tests for session indexing
|
- `tests/socket-service.test.ts` - 12 tests for session indexing
|
||||||
- `tests/drone-service.test.ts` - 6 tests for termination flow
|
- `tests/drone-service.test.ts` - 6 tests for termination flow
|
||||||
- `tests/drone-session.test.ts` - 2 new tests for requestTermination handler
|
- `tests/drone-session.test.ts` - 2 new tests for requestTermination handler
|
||||||
|
|
||||||
**Test Coverage**:
|
**Test Coverage**:
|
||||||
- ✅ Drone session storage and lookup by registration._id
|
|
||||||
- ✅ Code session storage and lookup by user._id
|
- ✅ Drone session storage and lookup by registration.\_id
|
||||||
|
- ✅ Code session storage and lookup by user.\_id
|
||||||
- ✅ Chat session index operations
|
- ✅ Chat session index operations
|
||||||
- ✅ Session cleanup on disconnect
|
- ✅ Session cleanup on disconnect
|
||||||
- ✅ requestTermination handler registration
|
- ✅ requestTermination handler registration
|
||||||
@ -137,6 +149,7 @@ Created new test files and utilities:
|
|||||||
**File**: `docs/socket-protocol.md`
|
**File**: `docs/socket-protocol.md`
|
||||||
|
|
||||||
Added:
|
Added:
|
||||||
|
|
||||||
- `requestTermination` to event maps (both directions)
|
- `requestTermination` to event maps (both directions)
|
||||||
- Complete drone termination flow sequence (Section 3.4)
|
- Complete drone termination flow sequence (Section 3.4)
|
||||||
- Message signatures for termination
|
- Message signatures for termination
|
||||||
@ -148,6 +161,7 @@ Added:
|
|||||||
**File**: `scripts/seed-socket-test-data.ts`
|
**File**: `scripts/seed-socket-test-data.ts`
|
||||||
|
|
||||||
Created script to seed test data:
|
Created script to seed test data:
|
||||||
|
|
||||||
- Test user account
|
- Test user account
|
||||||
- Test AI provider
|
- Test AI provider
|
||||||
- Test project (unique per run)
|
- Test project (unique per run)
|
||||||
@ -187,11 +201,13 @@ IDE (updates UI)
|
|||||||
## Files Changed
|
## Files Changed
|
||||||
|
|
||||||
### Core Implementation
|
### Core Implementation
|
||||||
|
|
||||||
- `gadget-code/src/services/socket.ts` - Dual-index architecture
|
- `gadget-code/src/services/socket.ts` - Dual-index architecture
|
||||||
- `gadget-code/src/lib/drone-session.ts` - requestTermination handler
|
- `gadget-code/src/lib/drone-session.ts` - requestTermination handler
|
||||||
- `gadget-code/src/services/drone.ts` - No changes (already correct)
|
- `gadget-code/src/services/drone.ts` - No changes (already correct)
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
- `tests/helpers/socket-test-helpers.ts` - NEW
|
- `tests/helpers/socket-test-helpers.ts` - NEW
|
||||||
- `tests/fixtures/index.ts` - NEW
|
- `tests/fixtures/index.ts` - NEW
|
||||||
- `tests/socket-service.test.ts` - NEW (12 tests)
|
- `tests/socket-service.test.ts` - NEW (12 tests)
|
||||||
@ -199,9 +215,11 @@ IDE (updates UI)
|
|||||||
- `tests/drone-session.test.ts` - MODIFIED (+2 tests)
|
- `tests/drone-session.test.ts` - MODIFIED (+2 tests)
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- `docs/socket-protocol.md` - Updated with termination flow and indexing
|
- `docs/socket-protocol.md` - Updated with termination flow and indexing
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
- `scripts/seed-socket-test-data.ts` - NEW
|
- `scripts/seed-socket-test-data.ts` - NEW
|
||||||
|
|
||||||
## Test Results
|
## Test Results
|
||||||
@ -241,6 +259,7 @@ Failed: tests/app.test.ts - Frontend build warning (unrelated)
|
|||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
The socket messaging system is now rock-solid with:
|
The socket messaging system is now rock-solid with:
|
||||||
|
|
||||||
- ✅ Correct session indexing and lookup
|
- ✅ Correct session indexing and lookup
|
||||||
- ✅ Complete test coverage (67 tests)
|
- ✅ Complete test coverage (67 tests)
|
||||||
- ✅ Proper error handling
|
- ✅ Proper error handling
|
||||||
|
|||||||
@ -51,6 +51,7 @@
|
|||||||
"mongoose": "^8.16.1",
|
"mongoose": "^8.16.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^2.0.1",
|
"multer": "^2.0.1",
|
||||||
|
"nanoid": "^5.1.11",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.3",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"pug": "^3.0.3",
|
"pug": "^3.0.3",
|
||||||
|
|||||||
@ -22,14 +22,14 @@
|
|||||||
* JSON object with created resource IDs for test cleanup
|
* JSON object with created resource IDs for test cleanup
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import mongoose from 'mongoose';
|
import mongoose from "mongoose";
|
||||||
import { config } from 'dotenv';
|
import { config } from "dotenv";
|
||||||
import { IUser } from '@gadget/api';
|
import { IUser } from "@gadget/api";
|
||||||
import User from '../src/models/user';
|
import User from "../src/models/user";
|
||||||
import Project from '../src/models/project';
|
import Project from "../src/models/project";
|
||||||
import ChatSession from '../src/models/chat-session';
|
import ChatSession from "../src/models/chat-session";
|
||||||
import DroneRegistration from '../src/models/drone-registration';
|
import DroneRegistration from "../src/models/drone-registration";
|
||||||
import { AiProvider } from '../src/models/ai-provider';
|
import { AiProvider } from "../src/models/ai-provider";
|
||||||
|
|
||||||
config();
|
config();
|
||||||
|
|
||||||
@ -45,43 +45,46 @@ interface SeedResult {
|
|||||||
|
|
||||||
async function seedSocketTestData(): Promise<SeedResult> {
|
async function seedSocketTestData(): Promise<SeedResult> {
|
||||||
const NOW = new Date();
|
const NOW = new Date();
|
||||||
const timestamp = NOW.toISOString().replace(/[:.]/g, '-');
|
const timestamp = NOW.toISOString().replace(/[:.]/g, "-");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Connect to database
|
// Connect to database
|
||||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/gadget-code');
|
await mongoose.connect(
|
||||||
console.log('Connected to MongoDB');
|
process.env.MONGODB_URI || "mongodb://localhost:27017/gadget-code",
|
||||||
|
);
|
||||||
|
console.log("Connected to MongoDB");
|
||||||
|
|
||||||
// Find or create test user
|
// Find or create test user
|
||||||
let user = await User.findOne({ email: 'test-socket@gadget-code.test' });
|
let user = await User.findOne({ email: "test-socket@gadget-code.test" });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = new User({
|
user = new User({
|
||||||
email: 'test-socket@gadget-code.test',
|
email: "test-socket@gadget-code.test",
|
||||||
displayName: 'Socket Test User',
|
displayName: "Socket Test User",
|
||||||
passwordSalt: 'test-salt',
|
passwordSalt: "test-salt",
|
||||||
passwordHash: 'test-hash',
|
passwordHash: "test-hash",
|
||||||
banned: false,
|
banned: false,
|
||||||
admin: false,
|
admin: false,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
});
|
});
|
||||||
await user.save();
|
await user.save();
|
||||||
console.log('Created test user:', user._id);
|
console.log("Created test user:", user._id);
|
||||||
} else {
|
} else {
|
||||||
console.log('Found existing test user:', user._id);
|
console.log("Found existing test user:", user._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or create a test AI provider
|
// Find or create a test AI provider
|
||||||
let provider = await AiProvider.findOne({ name: 'Test Socket Provider' });
|
let provider = await AiProvider.findOne({ name: "Test Socket Provider" });
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
provider = new AiProvider({
|
provider = new AiProvider({
|
||||||
name: 'Test Socket Provider',
|
name: "Test Socket Provider",
|
||||||
apiType: 'ollama',
|
apiType: "ollama",
|
||||||
baseUrl: 'http://localhost:11434',
|
baseUrl: "http://localhost:11434",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
models: [{
|
models: [
|
||||||
id: 'llama3.2',
|
{
|
||||||
name: 'Llama 3.2',
|
id: "llama3.2",
|
||||||
|
name: "Llama 3.2",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
canCallTools: true,
|
canCallTools: true,
|
||||||
hasVision: false,
|
hasVision: false,
|
||||||
@ -89,13 +92,14 @@ async function seedSocketTestData(): Promise<SeedResult> {
|
|||||||
hasThinking: false,
|
hasThinking: false,
|
||||||
isInstructTuned: true,
|
isInstructTuned: true,
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
lastModelRefresh: NOW,
|
lastModelRefresh: NOW,
|
||||||
});
|
});
|
||||||
await provider.save();
|
await provider.save();
|
||||||
console.log('Created test provider:', provider._id);
|
console.log("Created test provider:", provider._id);
|
||||||
} else {
|
} else {
|
||||||
console.log('Found existing test provider:', provider._id);
|
console.log("Found existing test provider:", provider._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create test project (unique per run)
|
// Create test project (unique per run)
|
||||||
@ -103,20 +107,20 @@ async function seedSocketTestData(): Promise<SeedResult> {
|
|||||||
user: user._id,
|
user: user._id,
|
||||||
slug: `socket-test-${timestamp}`,
|
slug: `socket-test-${timestamp}`,
|
||||||
name: `Socket Test Project ${timestamp}`,
|
name: `Socket Test Project ${timestamp}`,
|
||||||
gitUrl: 'https://github.com/test/socket-test.git',
|
gitUrl: "https://github.com/test/socket-test.git",
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
});
|
});
|
||||||
await project.save();
|
await project.save();
|
||||||
console.log('Created test project:', project._id);
|
console.log("Created test project:", project._id);
|
||||||
|
|
||||||
// Create test chat session (unique per run)
|
// Create test chat session (unique per run)
|
||||||
const chatSession = new ChatSession({
|
const chatSession = new ChatSession({
|
||||||
user: user._id,
|
user: user._id,
|
||||||
project: project._id,
|
project: project._id,
|
||||||
provider: provider._id,
|
provider: provider._id,
|
||||||
selectedModel: 'llama3.2',
|
selectedModel: "llama3.2",
|
||||||
mode: 'build',
|
mode: "build",
|
||||||
name: `Socket Test Session ${timestamp}`,
|
name: `Socket Test Session ${timestamp}`,
|
||||||
stats: {
|
stats: {
|
||||||
toolCallCount: 0,
|
toolCallCount: 0,
|
||||||
@ -127,7 +131,7 @@ async function seedSocketTestData(): Promise<SeedResult> {
|
|||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
});
|
});
|
||||||
await chatSession.save();
|
await chatSession.save();
|
||||||
console.log('Created test chat session:', chatSession._id);
|
console.log("Created test chat session:", chatSession._id);
|
||||||
|
|
||||||
// Create test drone registrations (unique per run)
|
// Create test drone registrations (unique per run)
|
||||||
const droneCount = 3;
|
const droneCount = 3;
|
||||||
@ -138,35 +142,35 @@ async function seedSocketTestData(): Promise<SeedResult> {
|
|||||||
user: user._id,
|
user: user._id,
|
||||||
hostname: `test-drone-${timestamp}-${i}`,
|
hostname: `test-drone-${timestamp}-${i}`,
|
||||||
workspaceDir: `/tmp/socket-test-${timestamp}-${i}`,
|
workspaceDir: `/tmp/socket-test-${timestamp}-${i}`,
|
||||||
status: 'available',
|
status: "available",
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
});
|
});
|
||||||
await drone.save();
|
await drone.save();
|
||||||
droneIds.push(drone._id.toHexString());
|
droneIds.push(drone._id);
|
||||||
console.log(`Created test drone ${i + 1}/${droneCount}:`, drone._id);
|
console.log(`Created test drone ${i + 1}/${droneCount}:`, drone._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: SeedResult = {
|
const result: SeedResult = {
|
||||||
userId: user._id.toHexString(),
|
userId: user._id,
|
||||||
projectId: project._id.toHexString(),
|
projectId: project._id,
|
||||||
chatSessionId: chatSession._id.toHexString(),
|
chatSessionId: chatSession._id,
|
||||||
droneIds,
|
droneIds,
|
||||||
providerId: provider._id.toHexString(),
|
providerId: provider._id,
|
||||||
createdAt: NOW.toISOString(),
|
createdAt: NOW.toISOString(),
|
||||||
note: 'TEST DATA - Safe to delete. Created for socket messaging tests.',
|
note: "TEST DATA - Safe to delete. Created for socket messaging tests.",
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('\n✅ Seed complete! Save this output for test cleanup:');
|
console.log("\n✅ Seed complete! Save this output for test cleanup:");
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Seed failed:', error);
|
console.error("❌ Seed failed:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
await mongoose.disconnect();
|
await mongoose.disconnect();
|
||||||
console.log('\nDisconnected from MongoDB');
|
console.log("\nDisconnected from MongoDB");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +180,6 @@ seedSocketTestData()
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Unhandled error:', error);
|
console.error("Unhandled error:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,8 +24,16 @@ class ChatSessionController extends DtpController {
|
|||||||
this.router.post("/", this.requireUser(), this.createSession.bind(this));
|
this.router.post("/", this.requireUser(), this.createSession.bind(this));
|
||||||
this.router.get("/:id", this.requireUser(), this.getSession.bind(this));
|
this.router.get("/:id", this.requireUser(), this.getSession.bind(this));
|
||||||
this.router.put("/:id", this.requireUser(), this.updateSession.bind(this));
|
this.router.put("/:id", this.requireUser(), this.updateSession.bind(this));
|
||||||
this.router.delete("/:id", this.requireUser(), this.deleteSession.bind(this));
|
this.router.delete(
|
||||||
this.router.get("/:id/turns", this.requireUser(), this.getSessionTurns.bind(this));
|
"/:id",
|
||||||
|
this.requireUser(),
|
||||||
|
this.deleteSession.bind(this),
|
||||||
|
);
|
||||||
|
this.router.get(
|
||||||
|
"/:id/turns",
|
||||||
|
this.requireUser(),
|
||||||
|
this.getSessionTurns.bind(this),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,7 +54,7 @@ class ChatSessionController extends DtpController {
|
|||||||
if (projectId) {
|
if (projectId) {
|
||||||
sessions = await ChatSessionService.getByProject(projectId);
|
sessions = await ChatSessionService.getByProject(projectId);
|
||||||
} else {
|
} else {
|
||||||
sessions = await ChatSessionService.getByUser(user._id.toHexString());
|
sessions = await ChatSessionService.getByUser(user._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@ -106,7 +114,7 @@ class ChatSessionController extends DtpController {
|
|||||||
: ChatSessionMode.Build;
|
: ChatSessionMode.Build;
|
||||||
|
|
||||||
const session = await ChatSessionService.create(
|
const session = await ChatSessionService.create(
|
||||||
user._id.toHexString(),
|
user._id,
|
||||||
projectId,
|
projectId,
|
||||||
providerId,
|
providerId,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
@ -134,7 +142,9 @@ class ChatSessionController extends DtpController {
|
|||||||
*/
|
*/
|
||||||
private async getSession(req: Request, res: Response): Promise<void> {
|
private async getSession(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
const id = Array.isArray(req.params.id)
|
||||||
|
? req.params.id[0]
|
||||||
|
: req.params.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -173,7 +183,9 @@ class ChatSessionController extends DtpController {
|
|||||||
*/
|
*/
|
||||||
private async updateSession(req: Request, res: Response): Promise<void> {
|
private async updateSession(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
const id = Array.isArray(req.params.id)
|
||||||
|
? req.params.id[0]
|
||||||
|
: req.params.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -201,7 +213,8 @@ class ChatSessionController extends DtpController {
|
|||||||
allowedUpdates.selectedModel = updates.selectedModel;
|
allowedUpdates.selectedModel = updates.selectedModel;
|
||||||
}
|
}
|
||||||
if (updates.mode !== undefined) {
|
if (updates.mode !== undefined) {
|
||||||
allowedUpdates.mode = ChatSessionMode[updates.mode as keyof typeof ChatSessionMode];
|
allowedUpdates.mode =
|
||||||
|
ChatSessionMode[updates.mode as keyof typeof ChatSessionMode];
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await ChatSessionService.update(id, allowedUpdates);
|
const session = await ChatSessionService.update(id, allowedUpdates);
|
||||||
@ -234,7 +247,9 @@ class ChatSessionController extends DtpController {
|
|||||||
*/
|
*/
|
||||||
private async deleteSession(req: Request, res: Response): Promise<void> {
|
private async deleteSession(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
const id = Array.isArray(req.params.id)
|
||||||
|
? req.params.id[0]
|
||||||
|
: req.params.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -273,7 +288,9 @@ class ChatSessionController extends DtpController {
|
|||||||
*/
|
*/
|
||||||
private async getSessionTurns(req: Request, res: Response): Promise<void> {
|
private async getSessionTurns(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
const id = Array.isArray(req.params.id)
|
||||||
|
? req.params.id[0]
|
||||||
|
: req.params.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@ -290,7 +307,9 @@ class ChatSessionController extends DtpController {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
this.log.error("failed to get chat session turns", { error: err.message });
|
this.log.error("failed to get chat session turns", {
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
|
|||||||
@ -3,8 +3,6 @@
|
|||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
import { DroneStatus } from "@gadget/api";
|
import { DroneStatus } from "@gadget/api";
|
||||||
import DroneService from "../../../services/drone.ts";
|
import DroneService from "../../../services/drone.ts";
|
||||||
import UserService from "../../../services/user.ts";
|
import UserService from "../../../services/user.ts";
|
||||||
@ -94,7 +92,7 @@ export class DroneApiControllerV1 extends DtpController {
|
|||||||
res.status(200).json({ success: true });
|
res.status(200).json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.error("failed to update drone status", {
|
this.log.error("failed to update drone status", {
|
||||||
_id: res.locals.registration._id.toHexString(),
|
_id: res.locals.registration._id,
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
res.status((error as Error).statusCode || 500).json({
|
res.status((error as Error).statusCode || 500).json({
|
||||||
@ -145,10 +143,9 @@ export class DroneApiControllerV1 extends DtpController {
|
|||||||
|
|
||||||
async postTerminate(req: Request, res: Response): Promise<void> {
|
async postTerminate(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const registrationId = Types.ObjectId.createFromHexString(
|
const result = await DroneService.requestTermination(
|
||||||
req.params.registrationId as string,
|
req.params.registrationId as string,
|
||||||
);
|
);
|
||||||
const result = await DroneService.requestTermination(registrationId);
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: result.success,
|
success: result.success,
|
||||||
message: result.message,
|
message: result.message,
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
|
||||||
import { DtpController } from "../../../lib/controller.js";
|
import { DtpController } from "../../../lib/controller.js";
|
||||||
import { ProjectStatus } from "@gadget/api";
|
import { IUser, ProjectStatus } from "@gadget/api";
|
||||||
import projectService from "../../../services/project.js";
|
import projectService from "../../../services/project.js";
|
||||||
|
|
||||||
export class ProjectApiControllerV1 extends DtpController {
|
export class ProjectApiControllerV1 extends DtpController {
|
||||||
@ -93,7 +93,8 @@ export class ProjectApiControllerV1 extends DtpController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.user._id.equals(req.user._id)) {
|
const user = project.user as IUser;
|
||||||
|
if (user._id !== req.user._id) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "access denied",
|
message: "access denied",
|
||||||
@ -126,7 +127,8 @@ export class ProjectApiControllerV1 extends DtpController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.user._id.equals(req.user._id)) {
|
const user = project.user as IUser;
|
||||||
|
if (user._id !== req.user._id) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "access denied",
|
message: "access denied",
|
||||||
@ -167,7 +169,8 @@ export class ProjectApiControllerV1 extends DtpController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!project.user._id.equals(req.user._id)) {
|
const user = project.user as IUser;
|
||||||
|
if (user._id !== req.user._id) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "access denied",
|
message: "access denied",
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
import { NextFunction, Request, RequestHandler, Response } from "express";
|
import { NextFunction, Request, RequestHandler, Response } from "express";
|
||||||
import { DtpController } from "../../lib/controller.ts";
|
import { DtpController } from "../../lib/controller.ts";
|
||||||
|
|
||||||
@ -29,8 +27,7 @@ export function populateUserById(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
assert(userId, "User ID is required");
|
assert(userId, "User ID is required");
|
||||||
try {
|
try {
|
||||||
const userIdObj = Types.ObjectId.createFromHexString(userId);
|
res.locals.userAccount = await UserService.getById(userId);
|
||||||
res.locals.userAccount = await UserService.getById(userIdObj);
|
|
||||||
if (options.requireObject && !res.locals.user) {
|
if (options.requireObject && !res.locals.user) {
|
||||||
const error = new Error("User not found");
|
const error = new Error("User not found");
|
||||||
error.statusCode = 404;
|
error.statusCode = 404;
|
||||||
@ -60,9 +57,7 @@ export function populateDroneRegistrationById(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
assert(registrationId, "Drone registration ID is required");
|
assert(registrationId, "Drone registration ID is required");
|
||||||
try {
|
try {
|
||||||
const registrationIdObj =
|
res.locals.registration = await DroneService.getById(registrationId);
|
||||||
Types.ObjectId.createFromHexString(registrationId);
|
|
||||||
res.locals.registration = await DroneService.getById(registrationIdObj);
|
|
||||||
return next();
|
return next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
controller.log.error("failed to populate User by ID", {
|
controller.log.error("failed to populate User by ID", {
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
import {
|
||||||
GadgetSocket,
|
GadgetSocket,
|
||||||
SocketSession,
|
SocketSession,
|
||||||
@ -14,6 +13,7 @@ import {
|
|||||||
IProject,
|
IProject,
|
||||||
IUser,
|
IUser,
|
||||||
ChatTurnStatus,
|
ChatTurnStatus,
|
||||||
|
GadgetId,
|
||||||
} from "@gadget/api";
|
} from "@gadget/api";
|
||||||
|
|
||||||
import SocketService from "../services/socket.ts";
|
import SocketService from "../services/socket.ts";
|
||||||
@ -25,7 +25,7 @@ export class CodeSession extends SocketSession {
|
|||||||
protected project: IProject | undefined;
|
protected project: IProject | undefined;
|
||||||
protected chatSession: IChatSession | undefined;
|
protected chatSession: IChatSession | undefined;
|
||||||
protected selectedDrone: IDroneRegistration | undefined;
|
protected selectedDrone: IDroneRegistration | undefined;
|
||||||
protected currentTurnId: Types.ObjectId | undefined;
|
protected currentTurnId: GadgetId | undefined;
|
||||||
|
|
||||||
constructor(socket: GadgetSocket, user: IUser) {
|
constructor(socket: GadgetSocket, user: IUser) {
|
||||||
super(socket, user);
|
super(socket, user);
|
||||||
@ -48,10 +48,7 @@ export class CodeSession extends SocketSession {
|
|||||||
/**
|
/**
|
||||||
* Sets the active chat session and project for this code session.
|
* Sets the active chat session and project for this code session.
|
||||||
*/
|
*/
|
||||||
setChatSession(
|
setChatSession(chatSession: IChatSession, project: IProject): void {
|
||||||
chatSession: IChatSession,
|
|
||||||
project: IProject,
|
|
||||||
): void {
|
|
||||||
this.chatSession = chatSession;
|
this.chatSession = chatSession;
|
||||||
this.project = project;
|
this.project = project;
|
||||||
}
|
}
|
||||||
@ -82,7 +79,7 @@ export class CodeSession extends SocketSession {
|
|||||||
this.selectedDrone = registration;
|
this.selectedDrone = registration;
|
||||||
this.chatSession = chatSession;
|
this.chatSession = chatSession;
|
||||||
this.project = project;
|
this.project = project;
|
||||||
SocketService.registerChatSession(chatSession._id.toHexString(), this);
|
SocketService.registerChatSession(chatSession._id, this);
|
||||||
droneSession.setChatSessionId(chatSession._id);
|
droneSession.setChatSessionId(chatSession._id);
|
||||||
}
|
}
|
||||||
cb(success, chatSessionId);
|
cb(success, chatSessionId);
|
||||||
@ -138,8 +135,8 @@ export class CodeSession extends SocketSession {
|
|||||||
this.currentTurnId = turn._id;
|
this.currentTurnId = turn._id;
|
||||||
|
|
||||||
this.log.info("ChatTurn created", {
|
this.log.info("ChatTurn created", {
|
||||||
turnId: turn._id.toHexString(),
|
turnId: turn._id,
|
||||||
chatSessionId: this.chatSession._id.toHexString(),
|
chatSessionId: this.chatSession._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
droneSession.setCurrentTurnId(turn._id);
|
droneSession.setCurrentTurnId(turn._id);
|
||||||
@ -153,12 +150,12 @@ export class CodeSession extends SocketSession {
|
|||||||
(success: boolean, message?: string) => {
|
(success: boolean, message?: string) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
this.log.info("work order accepted by drone", {
|
this.log.info("work order accepted by drone", {
|
||||||
turnId: turn._id.toHexString(),
|
turnId: turn._id,
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.log.error("work order rejected by drone", {
|
this.log.error("work order rejected by drone", {
|
||||||
turnId: turn._id.toHexString(),
|
turnId: turn._id,
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
turn.status = ChatTurnStatus.Error;
|
turn.status = ChatTurnStatus.Error;
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import path from "node:path";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { rateLimit } from "express-rate-limit";
|
import { rateLimit } from "express-rate-limit";
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
import {
|
||||||
Router,
|
Router,
|
||||||
Request,
|
Request,
|
||||||
@ -29,7 +28,7 @@ export interface CsrfTokenOptions {
|
|||||||
|
|
||||||
import { ApiClientStatus } from "../models/api-client.js";
|
import { ApiClientStatus } from "../models/api-client.js";
|
||||||
import { CsrfToken, ICsrfToken } from "../models/csrf-token.js";
|
import { CsrfToken, ICsrfToken } from "../models/csrf-token.js";
|
||||||
import { IUser } from "@gadget/api";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
|
|
||||||
import { DtpComponent } from "./component.js";
|
import { DtpComponent } from "./component.js";
|
||||||
import { DtpPaginationParameters } from "./pagination-parameters.js";
|
import { DtpPaginationParameters } from "./pagination-parameters.js";
|
||||||
@ -97,10 +96,7 @@ export abstract class DtpController implements DtpComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiClientIdObj = Types.ObjectId.createFromHexString(
|
const apiClient = await ApiClientService.getById(apiClientId as GadgetId);
|
||||||
apiClientId as string,
|
|
||||||
);
|
|
||||||
const apiClient = await ApiClientService.getById(apiClientIdObj);
|
|
||||||
if (!apiClient) {
|
if (!apiClient) {
|
||||||
this.log.error("API client not found", { _id: apiClientId });
|
this.log.error("API client not found", { _id: apiClientId });
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
|
|||||||
@ -2,8 +2,12 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
import {
|
||||||
import { IUser, IDroneRegistration, ChatTurnStatus } from "@gadget/api";
|
IUser,
|
||||||
|
IDroneRegistration,
|
||||||
|
ChatTurnStatus,
|
||||||
|
GadgetId,
|
||||||
|
} from "@gadget/api";
|
||||||
import {
|
import {
|
||||||
GadgetSocket,
|
GadgetSocket,
|
||||||
SocketSession,
|
SocketSession,
|
||||||
@ -15,8 +19,8 @@ import { ChatTurn } from "../models/chat-turn";
|
|||||||
export class DroneSession extends SocketSession {
|
export class DroneSession extends SocketSession {
|
||||||
protected type: SocketSessionType = SocketSessionType.Drone;
|
protected type: SocketSessionType = SocketSessionType.Drone;
|
||||||
registration: IDroneRegistration;
|
registration: IDroneRegistration;
|
||||||
chatSessionId: Types.ObjectId | undefined;
|
chatSessionId: GadgetId | undefined;
|
||||||
currentTurnId: Types.ObjectId | undefined;
|
currentTurnId: GadgetId | undefined;
|
||||||
|
|
||||||
constructor(socket: GadgetSocket, registration: IDroneRegistration) {
|
constructor(socket: GadgetSocket, registration: IDroneRegistration) {
|
||||||
super(socket, registration.user as IUser);
|
super(socket, registration.user as IUser);
|
||||||
@ -30,7 +34,10 @@ export class DroneSession extends SocketSession {
|
|||||||
this.socket.on("response", this.onResponse.bind(this));
|
this.socket.on("response", this.onResponse.bind(this));
|
||||||
this.socket.on("toolCall", this.onToolCall.bind(this));
|
this.socket.on("toolCall", this.onToolCall.bind(this));
|
||||||
this.socket.on("workOrderComplete", this.onWorkOrderComplete.bind(this));
|
this.socket.on("workOrderComplete", this.onWorkOrderComplete.bind(this));
|
||||||
this.socket.on("requestCrashRecovery", this.onRequestCrashRecovery.bind(this));
|
this.socket.on(
|
||||||
|
"requestCrashRecovery",
|
||||||
|
this.onRequestCrashRecovery.bind(this),
|
||||||
|
);
|
||||||
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +138,9 @@ export class DroneSession extends SocketSession {
|
|||||||
message?: string,
|
message?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.chatSessionId) {
|
if (!this.chatSessionId) {
|
||||||
this.log.warn("workOrderComplete event received but no chat session is active");
|
this.log.warn(
|
||||||
|
"workOrderComplete event received but no chat session is active",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,14 +168,14 @@ export class DroneSession extends SocketSession {
|
|||||||
/**
|
/**
|
||||||
* Sets the active chat session ID for this drone session.
|
* Sets the active chat session ID for this drone session.
|
||||||
*/
|
*/
|
||||||
setChatSessionId(chatSessionId: Types.ObjectId): void {
|
setChatSessionId(chatSessionId: GadgetId): void {
|
||||||
this.chatSessionId = chatSessionId;
|
this.chatSessionId = chatSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current turn ID being processed by this drone.
|
* Sets the current turn ID being processed by this drone.
|
||||||
*/
|
*/
|
||||||
setCurrentTurnId(turnId: Types.ObjectId): void {
|
setCurrentTurnId(turnId: GadgetId): void {
|
||||||
this.currentTurnId = turnId;
|
this.currentTurnId = turnId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,7 +265,7 @@ export class DroneSession extends SocketSession {
|
|||||||
*/
|
*/
|
||||||
async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
|
async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
|
||||||
this.log.info("requestTermination received, forwarding to drone", {
|
this.log.info("requestTermination received, forwarding to drone", {
|
||||||
registrationId: this.registration._id.toHexString(),
|
registrationId: this.registration._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.emit("requestTermination", (success: boolean) => {
|
this.socket.emit("requestTermination", (success: boolean) => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IAiModel,
|
IAiModel,
|
||||||
@ -58,6 +59,7 @@ export const AiModelSchema = new Schema<IAiModel>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const AiProviderSchema = new Schema<IAiProvider>({
|
export const AiProviderSchema = new Schema<IAiProvider>({
|
||||||
|
_id: { type: String, required: true, default: nanoid, unique: true },
|
||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
apiType: { type: String, enum: ["ollama", "openai"], required: true },
|
apiType: { type: String, enum: ["ollama", "openai"], required: true },
|
||||||
baseUrl: { type: String, required: true },
|
baseUrl: { type: String, required: true },
|
||||||
|
|||||||
@ -2,24 +2,22 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types, Schema, Document, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
|
||||||
import { IApiClient } from "./api-client.js";
|
import { IApiClient } from "./api-client.js";
|
||||||
const log = new DtpLog({
|
import { GadgetId } from "@gadget/api";
|
||||||
name: "ApiClientModel",
|
import { nanoid } from "nanoid";
|
||||||
slug: "apiClient",
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface IApiClientLog extends Document {
|
export interface IApiClientLog {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
client: IApiClient | Types.ObjectId;
|
client: IApiClient | GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
method: string;
|
method: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
export const ApiClientLogSchema = new Schema<IApiClientLog>({
|
export const ApiClientLogSchema = new Schema<IApiClientLog>({
|
||||||
client: { type: Types.ObjectId, required: true, index: 1, ref: "ApiClient" },
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
|
client: { type: String, required: true, index: 1, ref: "ApiClient" },
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now,
|
default: Date.now,
|
||||||
@ -33,11 +31,10 @@ export const ApiClientLogSchema = new Schema<IApiClientLog>({
|
|||||||
|
|
||||||
export const ApiClientLog = model<IApiClientLog>(
|
export const ApiClientLog = model<IApiClientLog>(
|
||||||
"ApiClientLog",
|
"ApiClientLog",
|
||||||
ApiClientLogSchema
|
ApiClientLogSchema,
|
||||||
);
|
);
|
||||||
export default ApiClientLog;
|
export default ApiClientLog;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await ApiClientLog.syncIndexes();
|
await ApiClientLog.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,13 +2,10 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types, Schema, Document, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
import { GadgetId } from "@gadget/api";
|
||||||
const log = new DtpLog({
|
import { nanoid } from "nanoid";
|
||||||
name: "ApiClientModel",
|
|
||||||
slug: "apiClient",
|
|
||||||
});
|
|
||||||
|
|
||||||
export enum ApiClientStatus {
|
export enum ApiClientStatus {
|
||||||
Active = "active",
|
Active = "active",
|
||||||
@ -16,8 +13,8 @@ export enum ApiClientStatus {
|
|||||||
Archived = "archived",
|
Archived = "archived",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApiClient extends Document {
|
export interface IApiClient {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
status: ApiClientStatus;
|
status: ApiClientStatus;
|
||||||
@ -26,6 +23,7 @@ export interface IApiClient extends Document {
|
|||||||
secret: string;
|
secret: string;
|
||||||
}
|
}
|
||||||
const ApiClientSchema = new Schema<IApiClient>({
|
const ApiClientSchema = new Schema<IApiClient>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
createdAt: { type: Date, required: true },
|
createdAt: { type: Date, required: true },
|
||||||
updatedAt: { type: Date, required: true },
|
updatedAt: { type: Date, required: true },
|
||||||
status: {
|
status: {
|
||||||
@ -44,6 +42,5 @@ export const ApiClient = model<IApiClient>("ApiClient", ApiClientSchema);
|
|||||||
export default ApiClient;
|
export default ApiClient;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await ApiClient.syncIndexes();
|
await ApiClient.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,18 +2,20 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types, Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
import { ChatSessionMode, IChatSession, IChatSessionPin } from "@gadget/api";
|
import { ChatSessionMode, IChatSession, IChatSessionPin } from "@gadget/api";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export const ChatSessionPinSchema = new Schema<IChatSessionPin>({
|
export const ChatSessionPinSchema = new Schema<IChatSessionPin>({
|
||||||
content: { type: String, required: true },
|
content: { type: String, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ChatSessionSchema = new Schema<IChatSession>({
|
export const ChatSessionSchema = new Schema<IChatSession>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
createdAt: { type: Date, default: Date.now, required: true },
|
createdAt: { type: Date, default: Date.now, required: true },
|
||||||
lastMessageAt: { type: Date },
|
lastMessageAt: { type: Date },
|
||||||
user: { type: Types.ObjectId, required: true, index: 1, ref: "User" },
|
user: { type: String, required: true, index: 1, ref: "User" },
|
||||||
project: { type: Types.ObjectId, required: false, index: 1, ref: "Project" },
|
project: { type: String, required: false, index: 1, ref: "Project" },
|
||||||
name: { type: String, default: "New Session", required: true },
|
name: { type: String, default: "New Session", required: true },
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -21,7 +23,7 @@ export const ChatSessionSchema = new Schema<IChatSession>({
|
|||||||
default: ChatSessionMode.Build,
|
default: ChatSessionMode.Build,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
provider: { type: Schema.Types.ObjectId, required: true, ref: "AiProvider" },
|
provider: { type: String, required: true, ref: "AiProvider" },
|
||||||
selectedModel: { type: String, required: true },
|
selectedModel: { type: String, required: true },
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: { type: Number, default: 0, required: true },
|
turnCount: { type: Number, default: 0, required: true },
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types, Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChatSessionMode,
|
ChatSessionMode,
|
||||||
@ -45,10 +45,10 @@ export const ChatSubagentProcessSchema = new Schema<IChatSubagentProcess>({
|
|||||||
|
|
||||||
export const ChatTurnSchema = new Schema<IChatTurn>({
|
export const ChatTurnSchema = new Schema<IChatTurn>({
|
||||||
createdAt: { type: Date, default: Date.now, required: true },
|
createdAt: { type: Date, default: Date.now, required: true },
|
||||||
user: { type: Types.ObjectId, required: true, ref: "User" },
|
user: { type: String, required: true, ref: "User" },
|
||||||
project: { type: Types.ObjectId, required: false, ref: "Project" },
|
project: { type: String, required: false, ref: "Project" },
|
||||||
session: { type: Types.ObjectId, required: true, ref: "ChatSession" },
|
session: { type: String, required: true, ref: "ChatSession" },
|
||||||
provider: { type: Types.ObjectId, required: true, ref: "AiProvider" },
|
provider: { type: String, required: true, ref: "AiProvider" },
|
||||||
llm: { type: String, required: true }, // id/name of the model used to process the prompt
|
llm: { type: String, required: true }, // id/name of the model used to process the prompt
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@ -2,22 +2,17 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, Types, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
import { IUser } from "@gadget/api";
|
|
||||||
const log = new DtpLog({
|
|
||||||
name: "CsrfTokenModel",
|
|
||||||
slug: "csrfToken",
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface ICsrfToken {
|
export interface ICsrfToken {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
created: Date;
|
created: Date;
|
||||||
expires: Date;
|
expires: Date;
|
||||||
claimed?: Date;
|
claimed?: Date;
|
||||||
token: string;
|
token: string;
|
||||||
user?: IUser | Types.ObjectId;
|
user?: IUser | GadgetId;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -37,7 +32,7 @@ const CsrfTokenSchema = new Schema<ICsrfToken>({
|
|||||||
expires: { type: Date, required: true, default: Date.now, index: -1 },
|
expires: { type: Date, required: true, default: Date.now, index: -1 },
|
||||||
claimed: { type: Date },
|
claimed: { type: Date },
|
||||||
token: { type: String, required: true, index: 1 },
|
token: { type: String, required: true, index: 1 },
|
||||||
user: { type: Types.ObjectId, ref: "User" },
|
user: { type: String, ref: "User" },
|
||||||
ip: { type: String, required: true },
|
ip: { type: String, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,6 +40,5 @@ export const CsrfToken = model<ICsrfToken>("CsrfToken", CsrfTokenSchema);
|
|||||||
export default CsrfToken;
|
export default CsrfToken;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await CsrfToken.syncIndexes();
|
await CsrfToken.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types, Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
import { IDroneMonitor, IMemoryMonitor } from "@gadget/api";
|
import { IDroneMonitor, IMemoryMonitor } from "@gadget/api";
|
||||||
|
|
||||||
export const MemoryMonitorSchema = new Schema<IMemoryMonitor>({
|
export const MemoryMonitorSchema = new Schema<IMemoryMonitor>({
|
||||||
@ -11,7 +11,7 @@ export const MemoryMonitorSchema = new Schema<IMemoryMonitor>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const DroneMonitorSchema = new Schema<IDroneMonitor>({
|
export const DroneMonitorSchema = new Schema<IDroneMonitor>({
|
||||||
registration: { type: Types.ObjectId, required: true, index: 1 },
|
registration: { type: String, required: true, index: 1 },
|
||||||
timestamp: { type: Date, required: true, index: -1 },
|
timestamp: { type: Date, required: true, index: -1 },
|
||||||
memory: {
|
memory: {
|
||||||
rss: { type: Number, required: true },
|
rss: { type: Number, required: true },
|
||||||
|
|||||||
@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
import { Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
import { DroneStatus, IDroneRegistration } from "@gadget/api";
|
import { DroneStatus, IDroneRegistration } from "@gadget/api";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export const DroneRegistrationSchema = new Schema<IDroneRegistration>({
|
export const DroneRegistrationSchema = new Schema<IDroneRegistration>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
createdAt: { type: Date, required: true },
|
createdAt: { type: Date, required: true },
|
||||||
updatedAt: { type: Date, required: false },
|
updatedAt: { type: Date, required: false },
|
||||||
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
|
user: { type: String, ref: "User", required: true },
|
||||||
hostname: { type: String, required: true },
|
hostname: { type: String, required: true },
|
||||||
workspaceDir: { type: String, required: true },
|
workspaceDir: { type: String, required: true },
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@ -2,16 +2,13 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, Types, Document, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
import { GadgetId } from "@gadget/api";
|
||||||
const log = new DtpLog({
|
import { nanoid } from "nanoid";
|
||||||
name: "EmailLogModel",
|
|
||||||
slug: "emailLog",
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface IEmailLog extends Document {
|
export interface IEmailLog {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
created: Date;
|
created: Date;
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
@ -20,6 +17,7 @@ export interface IEmailLog extends Document {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
}
|
}
|
||||||
export const EmailLogSchema = new Schema<IEmailLog>({
|
export const EmailLogSchema = new Schema<IEmailLog>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
created: { type: Date, default: Date.now, required: true, index: -1 },
|
created: { type: Date, default: Date.now, required: true, index: -1 },
|
||||||
from: { type: String, required: true },
|
from: { type: String, required: true },
|
||||||
to: { type: String, required: true },
|
to: { type: String, required: true },
|
||||||
@ -32,6 +30,5 @@ export const EmailLog = model<IEmailLog>("EmailLog", EmailLogSchema);
|
|||||||
export default EmailLog;
|
export default EmailLog;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await EmailLog.syncIndexes();
|
await EmailLog.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,14 +2,8 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, Types, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
import { IUser } from "@gadget/api";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
|
||||||
const log = new DtpLog({
|
|
||||||
name: "EmailVerificationModel",
|
|
||||||
slug: "emailVerification",
|
|
||||||
});
|
|
||||||
|
|
||||||
export enum EmailVerificationStatus {
|
export enum EmailVerificationStatus {
|
||||||
Pending = "pending",
|
Pending = "pending",
|
||||||
@ -19,13 +13,13 @@ export enum EmailVerificationStatus {
|
|||||||
|
|
||||||
export interface IEmailVerification {
|
export interface IEmailVerification {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
code: string;
|
code: string;
|
||||||
status: EmailVerificationStatus;
|
status: EmailVerificationStatus;
|
||||||
}
|
}
|
||||||
export const EmailVerificationSchema = new Schema<IEmailVerification>({
|
export const EmailVerificationSchema = new Schema<IEmailVerification>({
|
||||||
createdAt: { type: Date, required: true, default: Date.now },
|
createdAt: { type: Date, required: true, default: Date.now },
|
||||||
user: { type: Schema.Types.ObjectId, required: true, index: 1, ref: "User" },
|
user: { type: String, required: true, index: 1, ref: "User" },
|
||||||
code: { type: String, required: true, unique: true },
|
code: { type: String, required: true, unique: true },
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -42,6 +36,5 @@ export const EmailVerification = model<IEmailVerification>(
|
|||||||
export default EmailVerification;
|
export default EmailVerification;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await EmailVerification.syncIndexes();
|
await EmailVerification.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,18 +2,15 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, Types, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
|
||||||
import { IIdeSession } from "@gadget/api";
|
import { IIdeSession } from "@gadget/api";
|
||||||
const log = new DtpLog({
|
|
||||||
name: "IdeSessionModel",
|
|
||||||
slug: "model:ide-session",
|
|
||||||
});
|
|
||||||
|
|
||||||
const IdeSessionSchema = new Schema<IIdeSession>({
|
const IdeSessionSchema = new Schema<IIdeSession>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
createdAt: { type: Date, default: Date.now, required: true },
|
createdAt: { type: Date, default: Date.now, required: true },
|
||||||
user: { type: Types.ObjectId, required: true, ref: "User" },
|
user: { type: String, required: true, ref: "User" },
|
||||||
});
|
});
|
||||||
|
|
||||||
IdeSessionSchema.index({
|
IdeSessionSchema.index({
|
||||||
@ -25,6 +22,5 @@ export const IdeSession = model<IIdeSession>("IdeSession", IdeSessionSchema);
|
|||||||
export default IdeSession;
|
export default IdeSession;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await IdeSession.syncIndexes();
|
await IdeSession.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types, Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { ProjectStatus, IProject } from "@gadget/api";
|
import { ProjectStatus, IProject } from "@gadget/api";
|
||||||
|
|
||||||
export const ProjectSchema = new Schema<IProject>({
|
export const ProjectSchema = new Schema<IProject>({
|
||||||
createdAt: { type: Date, default: Date.now, required: true },
|
createdAt: { type: Date, default: Date.now, required: true },
|
||||||
user: { type: Types.ObjectId, required: true, index: 1, ref: "User" },
|
user: { type: String, required: true, index: 1, ref: "User" },
|
||||||
status: { type: String, enum: ProjectStatus, required: true },
|
status: { type: String, enum: ProjectStatus, required: true },
|
||||||
name: { type: String, default: "New Project", required: true },
|
name: { type: String, default: "New Project", required: true },
|
||||||
slug: {
|
slug: {
|
||||||
|
|||||||
@ -4,12 +4,8 @@
|
|||||||
|
|
||||||
import { Schema, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
|
||||||
import { IUser, IUserFlags } from "@gadget/api";
|
import { IUser, IUserFlags } from "@gadget/api";
|
||||||
const log = new DtpLog({
|
import { nanoid } from "nanoid";
|
||||||
name: "UserModel",
|
|
||||||
slug: "user",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const UserFlagsSchema = new Schema<IUserFlags>(
|
export const UserFlagsSchema = new Schema<IUserFlags>(
|
||||||
{
|
{
|
||||||
@ -22,6 +18,7 @@ export const UserFlagsSchema = new Schema<IUserFlags>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const UserSchema = new Schema<IUser>({
|
export const UserSchema = new Schema<IUser>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
email: { type: String, required: true },
|
email: { type: String, required: true },
|
||||||
email_lc: { type: String, required: true, lowercase: true, unique: true },
|
email_lc: { type: String, required: true, lowercase: true, unique: true },
|
||||||
passwordSalt: { type: String, required: true, select: false },
|
passwordSalt: { type: String, required: true, select: false },
|
||||||
@ -34,6 +31,5 @@ export const User = model<IUser>("User", UserSchema);
|
|||||||
export default User;
|
export default User;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await User.syncIndexes();
|
await User.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,27 +2,24 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, Types, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { IUser } from "@gadget/api";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
import { nanoid } from "nanoid";
|
||||||
const log = new DtpLog({
|
|
||||||
name: "WebTokenModel",
|
|
||||||
slug: "webToken",
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface IWebToken {
|
export interface IWebToken {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
created: Date;
|
created: Date;
|
||||||
expires: Date;
|
expires: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
export const WebTokenSchema = new Schema<IWebToken>({
|
export const WebTokenSchema = new Schema<IWebToken>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
created: { type: Date, required: true, index: 1 },
|
created: { type: Date, required: true, index: 1 },
|
||||||
expires: { type: Date, required: true, index: -1 },
|
expires: { type: Date, required: true, index: -1 },
|
||||||
user: { type: Schema.Types.ObjectId, required: true, ref: "User" },
|
user: { type: String, required: true, ref: "User" },
|
||||||
token: { type: String, required: true },
|
token: { type: String, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -30,6 +27,5 @@ export const WebToken = model<IWebToken>("WebToken", WebTokenSchema);
|
|||||||
export default WebToken;
|
export default WebToken;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await WebToken.syncIndexes();
|
await WebToken.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -2,21 +2,16 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Schema, Types, Document, model } from "mongoose";
|
import { Schema, model } from "mongoose";
|
||||||
|
|
||||||
import { DtpLog } from "../lib/log.js";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
import { IUser } from "@gadget/api";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
const log = new DtpLog({
|
export interface IWebVisit {
|
||||||
name: "WebVisitModel",
|
_id: GadgetId;
|
||||||
slug: "webVisit",
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface IWebVisit extends Document {
|
|
||||||
_id: Types.ObjectId;
|
|
||||||
created: Date;
|
created: Date;
|
||||||
url: string;
|
url: string;
|
||||||
user?: IUser | Types.ObjectId;
|
user?: IUser | GadgetId;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
referrer?: string;
|
referrer?: string;
|
||||||
ipAddress?: string;
|
ipAddress?: string;
|
||||||
@ -30,9 +25,10 @@ export interface IWebVisit extends Document {
|
|||||||
metroCode?: number;
|
metroCode?: number;
|
||||||
}
|
}
|
||||||
export const WebVisitSchema = new Schema<IWebVisit>({
|
export const WebVisitSchema = new Schema<IWebVisit>({
|
||||||
|
_id: { type: String, default: nanoid, required: true, unique: true },
|
||||||
created: { type: Date, default: Date.now },
|
created: { type: Date, default: Date.now },
|
||||||
url: { type: String, required: true },
|
url: { type: String, required: true },
|
||||||
user: { type: Types.ObjectId, index: 1, ref: "User" },
|
user: { type: String, index: 1, ref: "User" },
|
||||||
userAgent: { type: String },
|
userAgent: { type: String },
|
||||||
referrer: { type: String },
|
referrer: { type: String },
|
||||||
ipAddress: { type: String },
|
ipAddress: { type: String },
|
||||||
@ -48,6 +44,5 @@ export const WebVisitSchema = new Schema<IWebVisit>({
|
|||||||
export const WebVisit = model<IWebVisit>("WebVisit", WebVisitSchema);
|
export const WebVisit = model<IWebVisit>("WebVisit", WebVisitSchema);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.info("Syncing indexes...");
|
|
||||||
await WebVisit.syncIndexes();
|
await WebVisit.syncIndexes();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -5,11 +5,6 @@
|
|||||||
// import env, { getCountryName } from "../config/env.js";
|
// import env, { getCountryName } from "../config/env.js";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
|
||||||
import {
|
|
||||||
Types,
|
|
||||||
// MongooseQueryOptions,
|
|
||||||
// MongooseUpdateQueryOptions,
|
|
||||||
} from "mongoose";
|
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import { filterText } from "dtp-cleantext";
|
import { filterText } from "dtp-cleantext";
|
||||||
@ -22,6 +17,7 @@ import ApiClient, {
|
|||||||
import ApiClientLog, { IApiClientLog } from "../models/api-client-log.js";
|
import ApiClientLog, { IApiClientLog } from "../models/api-client-log.js";
|
||||||
|
|
||||||
import { DtpService } from "../lib/service.js";
|
import { DtpService } from "../lib/service.js";
|
||||||
|
import { GadgetId } from "@gadget/api";
|
||||||
|
|
||||||
class ApiClientService extends DtpService {
|
class ApiClientService extends DtpService {
|
||||||
get name(): string {
|
get name(): string {
|
||||||
@ -80,7 +76,7 @@ class ApiClientService extends DtpService {
|
|||||||
return newClient;
|
return newClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(clientId: Types.ObjectId): Promise<IApiClient | null> {
|
async getById(clientId: GadgetId): Promise<IApiClient | null> {
|
||||||
const client = await ApiClient.findOne({ _id: clientId });
|
const client = await ApiClient.findOne({ _id: clientId });
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,7 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
import { IChatSession, ChatSessionMode, GadgetId } from "@gadget/api";
|
||||||
import { IChatSession, ChatSessionMode } from "@gadget/api";
|
|
||||||
|
|
||||||
import { DtpService } from "../lib/service.js";
|
import { DtpService } from "../lib/service.js";
|
||||||
import ChatSession from "../models/chat-session.js";
|
import ChatSession from "../models/chat-session.js";
|
||||||
@ -31,36 +30,32 @@ class ChatSessionService extends DtpService {
|
|||||||
* Creates a new chat session.
|
* Creates a new chat session.
|
||||||
*/
|
*/
|
||||||
async create(
|
async create(
|
||||||
userId: string,
|
userId: GadgetId,
|
||||||
projectId: string,
|
projectId: GadgetId,
|
||||||
providerId: string,
|
providerId: GadgetId,
|
||||||
selectedModel: string,
|
selectedModel: string,
|
||||||
mode: ChatSessionMode = ChatSessionMode.Build,
|
mode: ChatSessionMode = ChatSessionMode.Build,
|
||||||
name?: string,
|
name?: string,
|
||||||
): Promise<IChatSession> {
|
): Promise<IChatSession> {
|
||||||
const userObj = new Types.ObjectId(userId);
|
|
||||||
const projectObj = new Types.ObjectId(projectId);
|
|
||||||
const providerObj = new Types.ObjectId(providerId);
|
|
||||||
|
|
||||||
// Validate project exists
|
// Validate project exists
|
||||||
const project = await Project.findById(projectObj);
|
const project = await Project.findById(projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(`Project not found: ${projectId}`);
|
throw new Error(`Project not found: ${projectId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate provider exists
|
// Validate provider exists
|
||||||
const provider = await AiProvider.findById(providerObj);
|
const provider = await AiProvider.findById(providerId);
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error(`AI Provider not found: ${providerId}`);
|
throw new Error(`AI Provider not found: ${providerId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = new ChatSession({
|
const session = new ChatSession({
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
user: userObj,
|
user: userId,
|
||||||
project: projectObj,
|
project: projectId,
|
||||||
name: name || "New Chat Session",
|
name: name || "New Chat Session",
|
||||||
mode,
|
mode,
|
||||||
provider: providerObj,
|
provider: providerId,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: 0,
|
turnCount: 0,
|
||||||
@ -74,7 +69,7 @@ class ChatSessionService extends DtpService {
|
|||||||
await session.save();
|
await session.save();
|
||||||
|
|
||||||
this.log.info("chat session created", {
|
this.log.info("chat session created", {
|
||||||
sessionId: session._id.toHexString(),
|
sessionId: session._id,
|
||||||
projectId,
|
projectId,
|
||||||
providerId,
|
providerId,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
@ -86,7 +81,7 @@ class ChatSessionService extends DtpService {
|
|||||||
/**
|
/**
|
||||||
* Gets a chat session by ID.
|
* Gets a chat session by ID.
|
||||||
*/
|
*/
|
||||||
async getById(chatSessionId: string): Promise<IChatSession> {
|
async getById(chatSessionId: GadgetId): Promise<IChatSession> {
|
||||||
const session = await ChatSession.findById(chatSessionId)
|
const session = await ChatSession.findById(chatSessionId)
|
||||||
.populate("user")
|
.populate("user")
|
||||||
.populate("project")
|
.populate("project")
|
||||||
@ -103,9 +98,8 @@ class ChatSessionService extends DtpService {
|
|||||||
/**
|
/**
|
||||||
* Gets all chat sessions for a project.
|
* Gets all chat sessions for a project.
|
||||||
*/
|
*/
|
||||||
async getByProject(projectId: string): Promise<IChatSession[]> {
|
async getByProject(projectId: GadgetId): Promise<IChatSession[]> {
|
||||||
const projectObj = new Types.ObjectId(projectId);
|
const sessions = await ChatSession.find({ project: projectId })
|
||||||
const sessions = await ChatSession.find({ project: projectObj })
|
|
||||||
.populate("user")
|
.populate("user")
|
||||||
.populate("project")
|
.populate("project")
|
||||||
.populate("provider")
|
.populate("provider")
|
||||||
@ -118,9 +112,8 @@ class ChatSessionService extends DtpService {
|
|||||||
/**
|
/**
|
||||||
* Gets all chat sessions for a user.
|
* Gets all chat sessions for a user.
|
||||||
*/
|
*/
|
||||||
async getByUser(userId: string): Promise<IChatSession[]> {
|
async getByUser(userId: GadgetId): Promise<IChatSession[]> {
|
||||||
const userObj = new Types.ObjectId(userId);
|
const sessions = await ChatSession.find({ user: userId })
|
||||||
const sessions = await ChatSession.find({ user: userObj })
|
|
||||||
.populate("user")
|
.populate("user")
|
||||||
.populate("project")
|
.populate("project")
|
||||||
.populate("provider")
|
.populate("provider")
|
||||||
@ -134,10 +127,10 @@ class ChatSessionService extends DtpService {
|
|||||||
* Updates a chat session.
|
* Updates a chat session.
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(
|
||||||
chatSessionId: string,
|
chatSessionId: GadgetId,
|
||||||
updates: Partial<{
|
updates: Partial<{
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: GadgetId;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
}>,
|
}>,
|
||||||
@ -153,7 +146,7 @@ class ChatSessionService extends DtpService {
|
|||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error(`AI Provider not found: ${updates.provider}`);
|
throw new Error(`AI Provider not found: ${updates.provider}`);
|
||||||
}
|
}
|
||||||
session.provider = new Types.ObjectId(updates.provider);
|
session.provider = updates.provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updates.name !== undefined) {
|
if (updates.name !== undefined) {
|
||||||
@ -179,14 +172,14 @@ class ChatSessionService extends DtpService {
|
|||||||
/**
|
/**
|
||||||
* Deletes a chat session.
|
* Deletes a chat session.
|
||||||
*/
|
*/
|
||||||
async delete(chatSessionId: string): Promise<void> {
|
async delete(chatSessionId: GadgetId): Promise<void> {
|
||||||
const session = await ChatSession.findByIdAndDelete(chatSessionId);
|
const session = await ChatSession.findByIdAndDelete(chatSessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(`Chat session not found: ${chatSessionId}`);
|
throw new Error(`Chat session not found: ${chatSessionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all turns associated with this session
|
// Delete all turns associated with this session
|
||||||
await ChatTurn.deleteMany({ session: new Types.ObjectId(chatSessionId) });
|
await ChatTurn.deleteMany({ session: chatSessionId });
|
||||||
|
|
||||||
this.log.info("chat session deleted", {
|
this.log.info("chat session deleted", {
|
||||||
sessionId: chatSessionId,
|
sessionId: chatSessionId,
|
||||||
@ -196,9 +189,8 @@ class ChatSessionService extends DtpService {
|
|||||||
/**
|
/**
|
||||||
* Gets all turns for a chat session.
|
* Gets all turns for a chat session.
|
||||||
*/
|
*/
|
||||||
async getTurns(chatSessionId: string): Promise<any[]> {
|
async getTurns(chatSessionId: GadgetId): Promise<any[]> {
|
||||||
const sessionObj = new Types.ObjectId(chatSessionId);
|
const turns = await ChatTurn.find({ session: chatSessionId })
|
||||||
const turns = await ChatTurn.find({ session: sessionObj })
|
|
||||||
.populate("user")
|
.populate("user")
|
||||||
.populate("project")
|
.populate("project")
|
||||||
.populate("provider")
|
.populate("provider")
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { PopulateOptions, Types } from "mongoose";
|
import { PopulateOptions } from "mongoose";
|
||||||
|
|
||||||
import { IUser, DroneStatus, IDroneRegistration } from "@gadget/api";
|
import { IUser, DroneStatus, IDroneRegistration, GadgetId } from "@gadget/api";
|
||||||
import DroneRegistration from "../models/drone-registration.ts";
|
import DroneRegistration from "../models/drone-registration.ts";
|
||||||
|
|
||||||
import { DtpService } from "../lib/service.ts";
|
import { DtpService } from "../lib/service.ts";
|
||||||
@ -91,7 +91,7 @@ class DroneService extends DtpService {
|
|||||||
* @param registrationId The _id of the drone registration to be fetched
|
* @param registrationId The _id of the drone registration to be fetched
|
||||||
* @returns the drone registration document
|
* @returns the drone registration document
|
||||||
*/
|
*/
|
||||||
async getById(registrationId: Types.ObjectId): Promise<IDroneRegistration> {
|
async getById(registrationId: GadgetId): Promise<IDroneRegistration> {
|
||||||
const registration = await DroneRegistration.findById(
|
const registration = await DroneRegistration.findById(
|
||||||
registrationId,
|
registrationId,
|
||||||
).populate(this.populateDroneRegistration);
|
).populate(this.populateDroneRegistration);
|
||||||
@ -146,7 +146,7 @@ class DroneService extends DtpService {
|
|||||||
* @returns Promise that resolves when termination is complete (success or timeout)
|
* @returns Promise that resolves when termination is complete (success or timeout)
|
||||||
*/
|
*/
|
||||||
async requestTermination(
|
async requestTermination(
|
||||||
registrationId: Types.ObjectId,
|
registrationId: GadgetId,
|
||||||
): Promise<{ success: boolean; message?: string }> {
|
): Promise<{ success: boolean; message?: string }> {
|
||||||
const registration = await this.getById(registrationId);
|
const registration = await this.getById(registrationId);
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ class DroneService extends DtpService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Drone is not connected - mark as offline immediately
|
// Drone is not connected - mark as offline immediately
|
||||||
this.log.warn("drone not connected, marking offline", {
|
this.log.warn("drone not connected, marking offline", {
|
||||||
registrationId: registrationId.toHexString(),
|
registrationId: registrationId,
|
||||||
hostname: registration.hostname,
|
hostname: registration.hostname,
|
||||||
});
|
});
|
||||||
await this.setStatus(registration, DroneStatus.Offline);
|
await this.setStatus(registration, DroneStatus.Offline);
|
||||||
@ -184,7 +184,7 @@ class DroneService extends DtpService {
|
|||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
this.log.warn("drone termination timed out, forcing offline", {
|
this.log.warn("drone termination timed out, forcing offline", {
|
||||||
registrationId: registrationId.toHexString(),
|
registrationId: registrationId,
|
||||||
hostname: registration.hostname,
|
hostname: registration.hostname,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ class DroneService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.log.info("drone accepted termination request", {
|
this.log.info("drone accepted termination request", {
|
||||||
registrationId: registrationId.toHexString(),
|
registrationId: registrationId,
|
||||||
hostname: registration.hostname,
|
hostname: registration.hostname,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -111,7 +111,7 @@ class ProjectService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.log.info("project updated", {
|
this.log.info("project updated", {
|
||||||
old: project.toObject ? project.toObject() : project,
|
old: project,
|
||||||
new: newProject.toObject(),
|
new: newProject.toObject(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ class ProjectService extends DtpService {
|
|||||||
|
|
||||||
this.log.info("project status updated", {
|
this.log.info("project status updated", {
|
||||||
project: {
|
project: {
|
||||||
_id: project._id.toHexString(),
|
_id: project._id,
|
||||||
slug: project.slug,
|
slug: project.slug,
|
||||||
},
|
},
|
||||||
old: project.status,
|
old: project.status,
|
||||||
@ -154,7 +154,9 @@ class ProjectService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findBySlug(slug: string, user: IUser): Promise<IProject | null> {
|
async findBySlug(slug: string, user: IUser): Promise<IProject | null> {
|
||||||
return Project.findOne({ slug, user: user._id }).populate(this.populateProject);
|
return Project.findOne({ slug, user: user._id }).populate(
|
||||||
|
this.populateProject,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(project: IProject): Promise<void> {
|
async delete(project: IProject): Promise<void> {
|
||||||
|
|||||||
@ -4,14 +4,13 @@
|
|||||||
|
|
||||||
import env from "../config/env.js";
|
import env from "../config/env.js";
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import geoip from "geoip-lite";
|
import geoip from "geoip-lite";
|
||||||
|
|
||||||
import { IUser } from "@gadget/api";
|
import { GadgetId, IUser } from "@gadget/api";
|
||||||
import { IWebToken, WebToken } from "../models/web-token.js";
|
import { IWebToken, WebToken } from "../models/web-token.js";
|
||||||
import { WebVisit } from "../models/web-visit.js";
|
import { WebVisit } from "../models/web-visit.js";
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ interface UserWebToken {
|
|||||||
_id: string;
|
_id: string;
|
||||||
email: string;
|
email: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
webToken: IWebToken | Types.ObjectId;
|
webToken: IWebToken | GadgetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SessionService extends DtpService {
|
class SessionService extends DtpService {
|
||||||
@ -68,7 +67,7 @@ class SessionService extends DtpService {
|
|||||||
try {
|
try {
|
||||||
const NOW = new Date();
|
const NOW = new Date();
|
||||||
const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken;
|
const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken;
|
||||||
const userId = Types.ObjectId.createFromHexString(payload._id);
|
const userId = payload._id;
|
||||||
|
|
||||||
const webToken = await WebToken.findOne({ _id: payload.webToken });
|
const webToken = await WebToken.findOne({ _id: payload.webToken });
|
||||||
if (!webToken) {
|
if (!webToken) {
|
||||||
@ -84,7 +83,7 @@ class SessionService extends DtpService {
|
|||||||
error.statusCode = 401;
|
error.statusCode = 401;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (!userId.equals(webToken.user._id)) {
|
if (userId !== (webToken.user as IUser)._id) {
|
||||||
const error = new Error("JSON Web Token ownership mismatch");
|
const error = new Error("JSON Web Token ownership mismatch");
|
||||||
error.name = "TokenOwnershipMismatch";
|
error.name = "TokenOwnershipMismatch";
|
||||||
error.statusCode = 401;
|
error.statusCode = 401;
|
||||||
|
|||||||
@ -3,9 +3,7 @@
|
|||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import env from "../config/env.ts";
|
import env from "../config/env.ts";
|
||||||
|
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
import { DisconnectReason, ExtendedError, Socket, Server } from "socket.io";
|
import { DisconnectReason, ExtendedError, Socket, Server } from "socket.io";
|
||||||
|
|
||||||
@ -32,8 +30,14 @@ type ChatSessionCodeSessionMap = Map<string, CodeSession>;
|
|||||||
class SocketService extends DtpService {
|
class SocketService extends DtpService {
|
||||||
private codeSessions: CodeSessionMap = new Map<string, CodeSession>();
|
private codeSessions: CodeSessionMap = new Map<string, CodeSession>();
|
||||||
private droneSessions: DroneSessionMap = new Map<string, DroneSession>();
|
private droneSessions: DroneSessionMap = new Map<string, DroneSession>();
|
||||||
private chatSessionIndex: ChatSessionCodeSessionMap = new Map<string, CodeSession>();
|
private chatSessionIndex: ChatSessionCodeSessionMap = new Map<
|
||||||
private droneRegistrationIndex: DroneSessionMap = new Map<string, DroneSession>();
|
string,
|
||||||
|
CodeSession
|
||||||
|
>();
|
||||||
|
private droneRegistrationIndex: DroneSessionMap = new Map<
|
||||||
|
string,
|
||||||
|
DroneSession
|
||||||
|
>();
|
||||||
private codeSessionUserIndex: CodeSessionMap = new Map<string, CodeSession>();
|
private codeSessionUserIndex: CodeSessionMap = new Map<string, CodeSession>();
|
||||||
|
|
||||||
private io?: Server<
|
private io?: Server<
|
||||||
@ -98,7 +102,7 @@ class SocketService extends DtpService {
|
|||||||
|
|
||||||
const session: CodeSession = new CodeSession(socket, user);
|
const session: CodeSession = new CodeSession(socket, user);
|
||||||
this.codeSessions.set(socket.id, session);
|
this.codeSessions.set(socket.id, session);
|
||||||
this.codeSessionUserIndex.set(user._id.toHexString(), session);
|
this.codeSessionUserIndex.set(user._id, session);
|
||||||
session.register();
|
session.register();
|
||||||
|
|
||||||
socket.data = { sessionType: SocketSessionType.Code };
|
socket.data = { sessionType: SocketSessionType.Code };
|
||||||
@ -122,12 +126,12 @@ class SocketService extends DtpService {
|
|||||||
* If not a User JWT, try to validate as a Drone session
|
* If not a User JWT, try to validate as a Drone session
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
const registrationId = Types.ObjectId.createFromHexString(token);
|
const registrationId = token;
|
||||||
const registration = await DroneService.getById(registrationId);
|
const registration = await DroneService.getById(registrationId);
|
||||||
|
|
||||||
const droneSession: DroneSession = new DroneSession(socket, registration);
|
const droneSession: DroneSession = new DroneSession(socket, registration);
|
||||||
this.droneSessions.set(socket.id, droneSession);
|
this.droneSessions.set(socket.id, droneSession);
|
||||||
this.droneRegistrationIndex.set(registration._id.toHexString(), droneSession);
|
this.droneRegistrationIndex.set(registration._id, droneSession);
|
||||||
droneSession.register();
|
droneSession.register();
|
||||||
|
|
||||||
socket.data = { sessionType: SocketSessionType.Drone };
|
socket.data = { sessionType: SocketSessionType.Drone };
|
||||||
@ -165,7 +169,7 @@ class SocketService extends DtpService {
|
|||||||
}
|
}
|
||||||
this.log.info("code socket connected", {
|
this.log.info("code socket connected", {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
userId: session.user._id.toHexString(),
|
userId: session.user._id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +182,7 @@ class SocketService extends DtpService {
|
|||||||
}
|
}
|
||||||
this.log.info("drone socket connected", {
|
this.log.info("drone socket connected", {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
registrationId: session.registration._id.toHexString(),
|
registrationId: session.registration._id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +200,7 @@ class SocketService extends DtpService {
|
|||||||
if (codeUserIndex) {
|
if (codeUserIndex) {
|
||||||
const session = this.codeSessions.get(socket.id);
|
const session = this.codeSessions.get(socket.id);
|
||||||
if (session) {
|
if (session) {
|
||||||
codeUserIndex.delete(session.user._id.toHexString());
|
codeUserIndex.delete(session.user._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -205,7 +209,7 @@ class SocketService extends DtpService {
|
|||||||
this.log.info("closing drone socket session", { id: socket.id });
|
this.log.info("closing drone socket session", { id: socket.id });
|
||||||
const droneSession = this.droneSessions.get(socket.id);
|
const droneSession = this.droneSessions.get(socket.id);
|
||||||
if (droneSession) {
|
if (droneSession) {
|
||||||
this.droneRegistrationIndex.delete(droneSession.registration._id.toHexString());
|
this.droneRegistrationIndex.delete(droneSession.registration._id);
|
||||||
}
|
}
|
||||||
this.droneSessions.delete(socket.id);
|
this.droneSessions.delete(socket.id);
|
||||||
return;
|
return;
|
||||||
@ -221,7 +225,7 @@ class SocketService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCodeSession(ideSession: IIdeSession): CodeSession {
|
getCodeSession(ideSession: IIdeSession): CodeSession {
|
||||||
const session = this.codeSessionUserIndex.get(ideSession._id.toHexString());
|
const session = this.codeSessionUserIndex.get(ideSession._id);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const error = new Error("code session not found");
|
const error = new Error("code session not found");
|
||||||
error.statusCode = 404;
|
error.statusCode = 404;
|
||||||
@ -231,7 +235,7 @@ class SocketService extends DtpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDroneSession(registration: IDroneRegistration): DroneSession {
|
getDroneSession(registration: IDroneRegistration): DroneSession {
|
||||||
const session = this.droneRegistrationIndex.get(registration._id.toHexString());
|
const session = this.droneRegistrationIndex.get(registration._id);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const error = new Error("drone session not found");
|
const error = new Error("drone session not found");
|
||||||
error.statusCode = 404;
|
error.statusCode = 404;
|
||||||
@ -250,11 +254,8 @@ class SocketService extends DtpService {
|
|||||||
/**
|
/**
|
||||||
* Gets a code session by its chat session ID.
|
* Gets a code session by its chat session ID.
|
||||||
*/
|
*/
|
||||||
getCodeSessionByChatSessionId(chatSessionId: Types.ObjectId | string): CodeSession {
|
getCodeSessionByChatSessionId(chatSessionId: string): CodeSession {
|
||||||
const chatSessionIdStr = typeof chatSessionId === "string"
|
const session = this.chatSessionIndex.get(chatSessionId);
|
||||||
? chatSessionId
|
|
||||||
: chatSessionId.toHexString();
|
|
||||||
const session = this.chatSessionIndex.get(chatSessionIdStr);
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const error = new Error("code session not found for chat session");
|
const error = new Error("code session not found for chat session");
|
||||||
error.statusCode = 404;
|
error.statusCode = 404;
|
||||||
|
|||||||
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
|
|
||||||
import { MongooseBaseQueryOptions, Types } from "mongoose";
|
import { MongooseBaseQueryOptions } from "mongoose";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { filterText } from "dtp-cleantext";
|
import { filterText } from "dtp-cleantext";
|
||||||
|
|
||||||
import User from "../models/user.ts";
|
import User from "../models/user.ts";
|
||||||
import { IUser } from "@gadget/api";
|
import { GadgetId, IUser, UserDocument } from "@gadget/api";
|
||||||
|
|
||||||
import ContactService from "./contact.ts";
|
import ContactService from "./contact.ts";
|
||||||
import CryptoService from "./crypto.ts";
|
import CryptoService from "./crypto.ts";
|
||||||
@ -52,7 +52,7 @@ class UserService extends DtpService {
|
|||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
): Promise<IUser> {
|
): Promise<UserDocument> {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
const error = new Error("must specify email address");
|
const error = new Error("must specify email address");
|
||||||
error.statusCode = 400;
|
error.statusCode = 400;
|
||||||
@ -254,7 +254,7 @@ class UserService extends DtpService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(userId: Types.ObjectId): Promise<IUser> {
|
async getById(userId: GadgetId): Promise<IUser> {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
const error = new Error("must specify email address");
|
const error = new Error("must specify email address");
|
||||||
error.statusCode = 400;
|
error.statusCode = 400;
|
||||||
|
|||||||
@ -45,7 +45,6 @@ import SocketService from "./services/socket.js";
|
|||||||
import SessionService, { SessionType } from "./services/session.js";
|
import SessionService, { SessionType } from "./services/session.js";
|
||||||
import StorageService from "./services/storage.js";
|
import StorageService from "./services/storage.js";
|
||||||
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { User } from "./models/user.js";
|
import { User } from "./models/user.js";
|
||||||
|
|
||||||
class DtpWebAppServer implements DtpComponent {
|
class DtpWebAppServer implements DtpComponent {
|
||||||
@ -123,7 +122,10 @@ class DtpWebAppServer implements DtpComponent {
|
|||||||
this.app.use(
|
this.app.use(
|
||||||
favicon(path.join(env.installDir, "assets", "icon", "icon-32x32.png")),
|
favicon(path.join(env.installDir, "assets", "icon", "icon-32x32.png")),
|
||||||
);
|
);
|
||||||
this.app.use("/assets", express.static(path.resolve(env.installDir, "assets")));
|
this.app.use(
|
||||||
|
"/assets",
|
||||||
|
express.static(path.resolve(env.installDir, "assets")),
|
||||||
|
);
|
||||||
this.app.use(
|
this.app.use(
|
||||||
"/client",
|
"/client",
|
||||||
express.static(path.resolve(env.installDir, "dist", "client")),
|
express.static(path.resolve(env.installDir, "dist", "client")),
|
||||||
@ -334,8 +336,7 @@ class DtpWebAppServer implements DtpComponent {
|
|||||||
if (token) {
|
if (token) {
|
||||||
req.user = await SessionService.verifyJsonWebToken(token);
|
req.user = await SessionService.verifyJsonWebToken(token);
|
||||||
} else {
|
} else {
|
||||||
const userId = Types.ObjectId.createFromHexString(req.session.user._id);
|
req.user = await User.findOne({ _id: req.session.user._id });
|
||||||
req.user = await User.findOne({ _id: userId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.locals.user = req.user;
|
res.locals.user = req.user;
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
import "./lib/db.js";
|
import "./lib/db.js";
|
||||||
|
|
||||||
@ -171,8 +170,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
throw new Error("client ID is required");
|
throw new Error("client ID is required");
|
||||||
}
|
}
|
||||||
const clientIdObj = Types.ObjectId.createFromHexString(clientId);
|
const client = await ApiClientService.getById(clientId);
|
||||||
const client = await ApiClientService.getById(clientIdObj);
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Client not found");
|
throw new Error("Client not found");
|
||||||
}
|
}
|
||||||
@ -191,8 +189,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
throw new Error("client ID is required");
|
throw new Error("client ID is required");
|
||||||
}
|
}
|
||||||
const clientIdObj = Types.ObjectId.createFromHexString(clientId);
|
const client = await ApiClientService.getById(clientId);
|
||||||
const client = await ApiClientService.getById(clientIdObj);
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Client not found");
|
throw new Error("Client not found");
|
||||||
}
|
}
|
||||||
@ -235,9 +232,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
|
|
||||||
const user = await UserService.create(email, password, displayName);
|
const user = await UserService.create(email, password, displayName);
|
||||||
|
|
||||||
this.log.info(
|
this.log.info(`user created: id:${user._id}, email:${user.email}`);
|
||||||
`user created: id:${user._id.toHexString()}, email:${user.email}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUserRemove(argv: string[]): Promise<void> {
|
async onUserRemove(argv: string[]): Promise<void> {
|
||||||
@ -393,7 +388,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
await provider.save();
|
await provider.save();
|
||||||
|
|
||||||
this.log.info("provider added", {
|
this.log.info("provider added", {
|
||||||
_id: provider._id.toHexString(),
|
_id: provider._id,
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
apiType: provider.apiType,
|
apiType: provider.apiType,
|
||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl,
|
||||||
@ -401,7 +396,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
|
|
||||||
// Auto-probe for models
|
// Auto-probe for models
|
||||||
this.log.info("probing provider for models...");
|
this.log.info("probing provider for models...");
|
||||||
await this.onProviderProbe([provider._id.toHexString()]);
|
await this.onProviderProbe([provider._id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onProviderList(_argv: string[]): Promise<void> {
|
async onProviderList(_argv: string[]): Promise<void> {
|
||||||
@ -435,8 +430,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
if (!providerId) {
|
if (!providerId) {
|
||||||
throw new Error("provider ID is required");
|
throw new Error("provider ID is required");
|
||||||
}
|
}
|
||||||
const providerIdObj = Types.ObjectId.createFromHexString(providerId);
|
const provider = await AiProvider.findById(providerId);
|
||||||
const provider = await AiProvider.findById(providerIdObj);
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error("Provider not found");
|
throw new Error("Provider not found");
|
||||||
}
|
}
|
||||||
@ -463,8 +457,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
if (!providerId) {
|
if (!providerId) {
|
||||||
throw new Error("provider ID is required");
|
throw new Error("provider ID is required");
|
||||||
}
|
}
|
||||||
const providerIdObj = Types.ObjectId.createFromHexString(providerId);
|
const provider = await AiProvider.findById(providerId).select("+apiKey");
|
||||||
const provider = await AiProvider.findById(providerIdObj).select("+apiKey");
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error("Provider not found");
|
throw new Error("Provider not found");
|
||||||
}
|
}
|
||||||
@ -479,8 +472,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
if (!providerId) {
|
if (!providerId) {
|
||||||
throw new Error("provider ID is required");
|
throw new Error("provider ID is required");
|
||||||
}
|
}
|
||||||
const providerIdObj = Types.ObjectId.createFromHexString(providerId);
|
const provider = await AiProvider.findById(providerId).select("+apiKey");
|
||||||
const provider = await AiProvider.findById(providerIdObj).select("+apiKey");
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error("Provider not found");
|
throw new Error("Provider not found");
|
||||||
}
|
}
|
||||||
@ -499,7 +491,7 @@ class DtpWebCli extends DtpProcess {
|
|||||||
|
|
||||||
const api = createAiApi(
|
const api = createAiApi(
|
||||||
{
|
{
|
||||||
_id: provider._id.toHexString(),
|
_id: provider._id,
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
sdk: provider.apiType,
|
sdk: provider.apiType,
|
||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl,
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import { CodeSession } from '../src/lib/code-session';
|
import { CodeSession } from "../src/lib/code-session";
|
||||||
import { IChatSession, IProject, IUser, IDroneRegistration, ChatTurnStatus } from '@gadget/api';
|
import {
|
||||||
import SocketService from '../src/services/socket';
|
IChatSession,
|
||||||
import { ChatTurn } from '../src/models/chat-turn';
|
IProject,
|
||||||
|
IUser,
|
||||||
|
IDroneRegistration,
|
||||||
|
ChatTurnStatus,
|
||||||
|
} from "@gadget/api";
|
||||||
|
import SocketService from "../src/services/socket";
|
||||||
|
import { ChatTurn } from "../src/models/chat-turn";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('../src/services/socket');
|
vi.mock("../src/services/socket");
|
||||||
vi.mock('../src/models/chat-turn');
|
vi.mock("../src/models/chat-turn");
|
||||||
|
|
||||||
describe('CodeSession', () => {
|
describe("CodeSession", () => {
|
||||||
let mockSocket: any;
|
let mockSocket: any;
|
||||||
let mockUser: IUser;
|
let mockUser: IUser;
|
||||||
let mockDrone: IDroneRegistration;
|
let mockDrone: IDroneRegistration;
|
||||||
@ -21,85 +28,90 @@ describe('CodeSession', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
mockSocket = {
|
mockSocket = {
|
||||||
id: 'test-socket-id',
|
id: "test-socket-id",
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
emit: vi.fn(),
|
emit: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockUser = {
|
mockUser = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
email: 'test@example.com',
|
email: "test@example.com",
|
||||||
displayName: 'Test User',
|
displayName: "Test User",
|
||||||
} as IUser;
|
} as IUser;
|
||||||
|
|
||||||
mockDrone = {
|
mockDrone = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
hostname: 'test-host',
|
hostname: "test-host",
|
||||||
workspaceDir: '/test/workspace',
|
workspaceDir: "/test/workspace",
|
||||||
status: 'available',
|
status: "available",
|
||||||
} as IDroneRegistration;
|
} as IDroneRegistration;
|
||||||
|
|
||||||
mockChatSession = {
|
mockChatSession = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
name: 'Test Session',
|
name: "Test Session",
|
||||||
mode: 'build',
|
mode: "build",
|
||||||
provider: new Types.ObjectId(),
|
provider: nanoid(),
|
||||||
selectedModel: 'llama3.1',
|
selectedModel: "llama3.1",
|
||||||
} as IChatSession;
|
} as IChatSession;
|
||||||
|
|
||||||
mockProject = {
|
mockProject = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
slug: 'test-project',
|
slug: "test-project",
|
||||||
name: 'Test Project',
|
name: "Test Project",
|
||||||
} as IProject;
|
} as IProject;
|
||||||
|
|
||||||
codeSession = new CodeSession(mockSocket, mockUser);
|
codeSession = new CodeSession(mockSocket, mockUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setSelectedDrone', () => {
|
describe("setSelectedDrone", () => {
|
||||||
it('should set the selected drone', () => {
|
it("should set the selected drone", () => {
|
||||||
codeSession.setSelectedDrone(mockDrone);
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
// Can't directly access private property, but we can verify through behavior
|
// Can't directly access private property, but we can verify through behavior
|
||||||
expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow();
|
expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setChatSession', () => {
|
describe("setChatSession", () => {
|
||||||
it('should set the chat session and project', () => {
|
it("should set the chat session and project", () => {
|
||||||
codeSession.setChatSession(mockChatSession, mockProject);
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
expect(() => codeSession.setChatSession(mockChatSession, mockProject)).not.toThrow();
|
expect(() =>
|
||||||
|
codeSession.setChatSession(mockChatSession, mockProject),
|
||||||
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onSubmitPrompt', () => {
|
describe("onSubmitPrompt", () => {
|
||||||
it('should throw error if no drone is selected', async () => {
|
it("should throw error if no drone is selected", async () => {
|
||||||
codeSession.setChatSession(mockChatSession, mockProject);
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
|
||||||
await expect(codeSession.onSubmitPrompt('test prompt'))
|
await expect(codeSession.onSubmitPrompt("test prompt")).rejects.toThrow(
|
||||||
.rejects.toThrow('No drone selected');
|
"No drone selected",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if no chat session is active', async () => {
|
it("should throw error if no chat session is active", async () => {
|
||||||
codeSession.setSelectedDrone(mockDrone);
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
|
|
||||||
await expect(codeSession.onSubmitPrompt('test prompt'))
|
await expect(codeSession.onSubmitPrompt("test prompt")).rejects.toThrow(
|
||||||
.rejects.toThrow('No chat session active');
|
"No chat session active",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if no project is selected', async () => {
|
it("should throw error if no project is selected", async () => {
|
||||||
codeSession.setSelectedDrone(mockDrone);
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
codeSession.setChatSession(mockChatSession, undefined as any);
|
codeSession.setChatSession(mockChatSession, undefined as any);
|
||||||
|
|
||||||
await expect(codeSession.onSubmitPrompt('test prompt'))
|
await expect(codeSession.onSubmitPrompt("test prompt")).rejects.toThrow(
|
||||||
.rejects.toThrow('No project selected');
|
"No project selected",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a ChatTurn and emit processWorkOrder to drone', async () => {
|
it("should create a ChatTurn and emit processWorkOrder to drone", async () => {
|
||||||
codeSession.setSelectedDrone(mockDrone);
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
codeSession.setChatSession(mockChatSession, mockProject);
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
|
||||||
const mockTurn = {
|
const mockTurn = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
save: vi.fn().mockResolvedValue(undefined),
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
vi.mocked(ChatTurn).mockImplementation(function () {
|
vi.mocked(ChatTurn).mockImplementation(function () {
|
||||||
@ -113,11 +125,14 @@ describe('CodeSession', () => {
|
|||||||
setChatSessionId: vi.fn(),
|
setChatSessionId: vi.fn(),
|
||||||
setCurrentTurnId: vi.fn(),
|
setCurrentTurnId: vi.fn(),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
||||||
|
mockDroneSession as any,
|
||||||
|
);
|
||||||
|
|
||||||
await codeSession.onSubmitPrompt('test prompt');
|
await codeSession.onSubmitPrompt("test prompt");
|
||||||
|
|
||||||
expect(ChatTurn).toHaveBeenCalledWith(expect.objectContaining({
|
expect(ChatTurn).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
user: mockUser._id,
|
user: mockUser._id,
|
||||||
project: mockProject._id,
|
project: mockProject._id,
|
||||||
session: mockChatSession._id,
|
session: mockChatSession._id,
|
||||||
@ -125,30 +140,31 @@ describe('CodeSession', () => {
|
|||||||
llm: mockChatSession.selectedModel,
|
llm: mockChatSession.selectedModel,
|
||||||
status: ChatTurnStatus.Processing,
|
status: ChatTurnStatus.Processing,
|
||||||
prompts: {
|
prompts: {
|
||||||
user: 'test prompt',
|
user: "test prompt",
|
||||||
system: undefined,
|
system: undefined,
|
||||||
},
|
},
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockTurn.save).toHaveBeenCalled();
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
|
expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
|
||||||
'processWorkOrder',
|
"processWorkOrder",
|
||||||
mockDrone,
|
mockDrone,
|
||||||
mockProject,
|
mockProject,
|
||||||
mockChatSession,
|
mockChatSession,
|
||||||
mockTurn,
|
mockTurn,
|
||||||
expect.any(Function)
|
expect.any(Function),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update ChatTurn to Error status if drone rejects work order', async () => {
|
it("should update ChatTurn to Error status if drone rejects work order", async () => {
|
||||||
codeSession.setSelectedDrone(mockDrone);
|
codeSession.setSelectedDrone(mockDrone);
|
||||||
codeSession.setChatSession(mockChatSession, mockProject);
|
codeSession.setChatSession(mockChatSession, mockProject);
|
||||||
|
|
||||||
const mockTurn = {
|
const mockTurn = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
status: ChatTurnStatus.Processing,
|
status: ChatTurnStatus.Processing,
|
||||||
response: '',
|
response: "",
|
||||||
save: vi.fn().mockResolvedValue(undefined),
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
vi.mocked(ChatTurn).mockImplementation(function () {
|
vi.mocked(ChatTurn).mockImplementation(function () {
|
||||||
@ -159,59 +175,75 @@ describe('CodeSession', () => {
|
|||||||
socket: {
|
socket: {
|
||||||
emit: vi.fn((event, ...args) => {
|
emit: vi.fn((event, ...args) => {
|
||||||
const callback = args[args.length - 1];
|
const callback = args[args.length - 1];
|
||||||
callback(false, 'Drone is busy');
|
callback(false, "Drone is busy");
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
setChatSessionId: vi.fn(),
|
setChatSessionId: vi.fn(),
|
||||||
setCurrentTurnId: vi.fn(),
|
setCurrentTurnId: vi.fn(),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
||||||
|
mockDroneSession as any,
|
||||||
|
);
|
||||||
|
|
||||||
await codeSession.onSubmitPrompt('test prompt');
|
await codeSession.onSubmitPrompt("test prompt");
|
||||||
|
|
||||||
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
||||||
expect(mockTurn.response).toBe('Drone is busy');
|
expect(mockTurn.response).toBe("Drone is busy");
|
||||||
expect(mockTurn.save).toHaveBeenCalled();
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onRequestSessionLock', () => {
|
describe("onRequestSessionLock", () => {
|
||||||
it('should set selected drone, chat session, and project on success', () => {
|
it("should set selected drone, chat session, and project on success", () => {
|
||||||
const mockDroneSession = {
|
const mockDroneSession = {
|
||||||
socket: {
|
socket: {
|
||||||
emit: vi.fn((event, ...args) => {
|
emit: vi.fn((event, ...args) => {
|
||||||
const callback = args[args.length - 1];
|
const callback = args[args.length - 1];
|
||||||
callback(true, mockChatSession._id.toHexString());
|
callback(true, mockChatSession._id);
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
setChatSessionId: vi.fn(),
|
setChatSessionId: vi.fn(),
|
||||||
setCurrentTurnId: vi.fn(),
|
setCurrentTurnId: vi.fn(),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
||||||
|
mockDroneSession as any,
|
||||||
|
);
|
||||||
|
|
||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
codeSession.onRequestSessionLock(mockDrone, mockProject, mockChatSession, callback);
|
codeSession.onRequestSessionLock(
|
||||||
|
mockDrone,
|
||||||
|
mockProject,
|
||||||
|
mockChatSession,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(true, mockChatSession._id.toHexString());
|
expect(callback).toHaveBeenCalledWith(true, mockChatSession._id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set session data on failure', () => {
|
it("should not set session data on failure", () => {
|
||||||
const mockDroneSession = {
|
const mockDroneSession = {
|
||||||
socket: {
|
socket: {
|
||||||
emit: vi.fn((event, ...args) => {
|
emit: vi.fn((event, ...args) => {
|
||||||
const callback = args[args.length - 1];
|
const callback = args[args.length - 1];
|
||||||
callback(false, '');
|
callback(false, "");
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
setChatSessionId: vi.fn(),
|
setChatSessionId: vi.fn(),
|
||||||
setCurrentTurnId: vi.fn(),
|
setCurrentTurnId: vi.fn(),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getDroneSession).mockReturnValue(mockDroneSession as any);
|
vi.mocked(SocketService.getDroneSession).mockReturnValue(
|
||||||
|
mockDroneSession as any,
|
||||||
|
);
|
||||||
|
|
||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
codeSession.onRequestSessionLock(mockDrone, mockProject, mockChatSession, callback);
|
codeSession.onRequestSessionLock(
|
||||||
|
mockDrone,
|
||||||
|
mockProject,
|
||||||
|
mockChatSession,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(false, '');
|
expect(callback).toHaveBeenCalledWith(false, "");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,134 +1,174 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import { DroneSession } from '../src/lib/drone-session';
|
import { DroneSession } from "../src/lib/drone-session";
|
||||||
import { IDroneRegistration, IUser, ChatTurnStatus } from '@gadget/api';
|
import {
|
||||||
import SocketService from '../src/services/socket';
|
IDroneRegistration,
|
||||||
import { ChatTurn } from '../src/models/chat-turn';
|
IUser,
|
||||||
|
ChatTurnStatus,
|
||||||
|
GadgetId,
|
||||||
|
} from "@gadget/api";
|
||||||
|
import SocketService from "../src/services/socket";
|
||||||
|
import { ChatTurn } from "../src/models/chat-turn";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('../src/services/socket');
|
vi.mock("../src/services/socket");
|
||||||
vi.mock('../src/models/chat-turn');
|
vi.mock("../src/models/chat-turn");
|
||||||
|
|
||||||
describe('DroneSession', () => {
|
describe("DroneSession", () => {
|
||||||
let mockSocket: any;
|
let mockSocket: any;
|
||||||
let mockRegistration: IDroneRegistration;
|
let mockRegistration: IDroneRegistration;
|
||||||
let mockUser: IUser;
|
let mockUser: IUser;
|
||||||
let droneSession: DroneSession;
|
let droneSession: DroneSession;
|
||||||
let mockChatSessionId: Types.ObjectId;
|
let mockChatSessionId: GadgetId;
|
||||||
let mockTurnId: Types.ObjectId;
|
let mockTurnId: GadgetId;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
mockSocket = {
|
mockSocket = {
|
||||||
id: 'test-drone-socket',
|
id: "test-drone-socket",
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
emit: vi.fn(),
|
emit: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
mockUser = {
|
mockUser = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
email: 'test@example.com',
|
email: "test@example.com",
|
||||||
displayName: 'Test User',
|
displayName: "Test User",
|
||||||
} as IUser;
|
} as IUser;
|
||||||
|
|
||||||
mockRegistration = {
|
mockRegistration = {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
hostname: 'test-host',
|
hostname: "test-host",
|
||||||
workspaceDir: '/test/workspace',
|
workspaceDir: "/test/workspace",
|
||||||
status: 'available',
|
status: "available",
|
||||||
} as IDroneRegistration;
|
} as IDroneRegistration;
|
||||||
|
|
||||||
mockChatSessionId = new Types.ObjectId();
|
mockChatSessionId = nanoid();
|
||||||
mockTurnId = new Types.ObjectId();
|
mockTurnId = nanoid();
|
||||||
|
|
||||||
droneSession = new DroneSession(mockSocket, mockRegistration);
|
droneSession = new DroneSession(mockSocket, mockRegistration);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('register', () => {
|
describe("register", () => {
|
||||||
it('should register event handlers for drone events', () => {
|
it("should register event handlers for drone events", () => {
|
||||||
droneSession.register();
|
droneSession.register();
|
||||||
|
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('thinking', expect.any(Function));
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('response', expect.any(Function));
|
"thinking",
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('toolCall', expect.any(Function));
|
expect.any(Function),
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('workOrderComplete', expect.any(Function));
|
);
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('requestCrashRecovery', expect.any(Function));
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
expect(mockSocket.on).toHaveBeenCalledWith('requestTermination', expect.any(Function));
|
"response",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
|
"toolCall",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
|
"workOrderComplete",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
|
"requestCrashRecovery",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
|
expect(mockSocket.on).toHaveBeenCalledWith(
|
||||||
|
"requestTermination",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setChatSessionId', () => {
|
describe("setChatSessionId", () => {
|
||||||
it('should set the chat session ID', () => {
|
it("should set the chat session ID", () => {
|
||||||
droneSession.setChatSessionId(mockChatSessionId);
|
droneSession.setChatSessionId(mockChatSessionId);
|
||||||
expect(() => droneSession.setChatSessionId(mockChatSessionId)).not.toThrow();
|
expect(() =>
|
||||||
|
droneSession.setChatSessionId(mockChatSessionId),
|
||||||
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setCurrentTurnId', () => {
|
describe("setCurrentTurnId", () => {
|
||||||
it('should set the current turn ID', () => {
|
it("should set the current turn ID", () => {
|
||||||
droneSession.setCurrentTurnId(mockTurnId);
|
droneSession.setCurrentTurnId(mockTurnId);
|
||||||
expect(() => droneSession.setCurrentTurnId(mockTurnId)).not.toThrow();
|
expect(() => droneSession.setCurrentTurnId(mockTurnId)).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onThinking', () => {
|
describe("onThinking", () => {
|
||||||
it('should route thinking event to code session and update ChatTurn', async () => {
|
it("should route thinking event to code session and update ChatTurn", async () => {
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: { emit: vi.fn() },
|
socket: { emit: vi.fn() },
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(mockCodeSession as any);
|
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||||
|
mockCodeSession as any,
|
||||||
|
);
|
||||||
vi.mocked(ChatTurn.findByIdAndUpdate).mockResolvedValue({} as any);
|
vi.mocked(ChatTurn.findByIdAndUpdate).mockResolvedValue({} as any);
|
||||||
|
|
||||||
droneSession.setChatSessionId(mockChatSessionId);
|
droneSession.setChatSessionId(mockChatSessionId);
|
||||||
droneSession.setCurrentTurnId(mockTurnId);
|
droneSession.setCurrentTurnId(mockTurnId);
|
||||||
|
|
||||||
await droneSession.onThinking('thinking content');
|
await droneSession.onThinking("thinking content");
|
||||||
|
|
||||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(mockChatSessionId);
|
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith('thinking', 'thinking content');
|
mockChatSessionId,
|
||||||
|
);
|
||||||
|
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||||
|
"thinking",
|
||||||
|
"thinking content",
|
||||||
|
);
|
||||||
expect(ChatTurn.findByIdAndUpdate).toHaveBeenCalledWith(mockTurnId, {
|
expect(ChatTurn.findByIdAndUpdate).toHaveBeenCalledWith(mockTurnId, {
|
||||||
thinking: 'thinking content',
|
thinking: "thinking content",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log warning if no chat session is active', async () => {
|
it("should log warning if no chat session is active", async () => {
|
||||||
await droneSession.onThinking('thinking content');
|
await droneSession.onThinking("thinking content");
|
||||||
// No exception thrown, warning logged internally
|
// No exception thrown, warning logged internally
|
||||||
expect(mockSocket.emit).not.toHaveBeenCalled();
|
expect(mockSocket.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onResponse', () => {
|
describe("onResponse", () => {
|
||||||
it('should route response event to code session and update ChatTurn', async () => {
|
it("should route response event to code session and update ChatTurn", async () => {
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: { emit: vi.fn() },
|
socket: { emit: vi.fn() },
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(mockCodeSession as any);
|
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||||
|
mockCodeSession as any,
|
||||||
|
);
|
||||||
vi.mocked(ChatTurn.findByIdAndUpdate).mockResolvedValue({} as any);
|
vi.mocked(ChatTurn.findByIdAndUpdate).mockResolvedValue({} as any);
|
||||||
|
|
||||||
droneSession.setChatSessionId(mockChatSessionId);
|
droneSession.setChatSessionId(mockChatSessionId);
|
||||||
droneSession.setCurrentTurnId(mockTurnId);
|
droneSession.setCurrentTurnId(mockTurnId);
|
||||||
|
|
||||||
await droneSession.onResponse('response content');
|
await droneSession.onResponse("response content");
|
||||||
|
|
||||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(mockChatSessionId);
|
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith('response', 'response content');
|
mockChatSessionId,
|
||||||
|
);
|
||||||
|
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||||
|
"response",
|
||||||
|
"response content",
|
||||||
|
);
|
||||||
expect(ChatTurn.findByIdAndUpdate).toHaveBeenCalledWith(mockTurnId, {
|
expect(ChatTurn.findByIdAndUpdate).toHaveBeenCalledWith(mockTurnId, {
|
||||||
response: 'response content',
|
response: "response content",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log warning if no chat session is active', async () => {
|
it("should log warning if no chat session is active", async () => {
|
||||||
await droneSession.onResponse('response content');
|
await droneSession.onResponse("response content");
|
||||||
// No exception thrown, warning logged internally
|
// No exception thrown, warning logged internally
|
||||||
expect(mockSocket.emit).not.toHaveBeenCalled();
|
expect(mockSocket.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onToolCall', () => {
|
describe("onToolCall", () => {
|
||||||
it('should route toolCall event to code session and update ChatTurn', async () => {
|
it("should route toolCall event to code session and update ChatTurn", async () => {
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: { emit: vi.fn() },
|
socket: { emit: vi.fn() },
|
||||||
};
|
};
|
||||||
@ -137,37 +177,52 @@ describe('DroneSession', () => {
|
|||||||
stats: { toolCallCount: 0 },
|
stats: { toolCallCount: 0 },
|
||||||
save: vi.fn().mockResolvedValue(undefined),
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(mockCodeSession as any);
|
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||||
|
mockCodeSession as any,
|
||||||
|
);
|
||||||
vi.mocked(ChatTurn.findById).mockResolvedValue(mockTurn as any);
|
vi.mocked(ChatTurn.findById).mockResolvedValue(mockTurn as any);
|
||||||
|
|
||||||
droneSession.setChatSessionId(mockChatSessionId);
|
droneSession.setChatSessionId(mockChatSessionId);
|
||||||
droneSession.setCurrentTurnId(mockTurnId);
|
droneSession.setCurrentTurnId(mockTurnId);
|
||||||
|
|
||||||
await droneSession.onToolCall('call-123', 'readFile', '{"path":"test.ts"}', 'file contents');
|
await droneSession.onToolCall(
|
||||||
|
"call-123",
|
||||||
|
"readFile",
|
||||||
|
'{"path":"test.ts"}',
|
||||||
|
"file contents",
|
||||||
|
);
|
||||||
|
|
||||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(mockChatSessionId);
|
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith('toolCall', 'call-123', 'readFile', '{"path":"test.ts"}', 'file contents');
|
mockChatSessionId,
|
||||||
|
);
|
||||||
|
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||||
|
"toolCall",
|
||||||
|
"call-123",
|
||||||
|
"readFile",
|
||||||
|
'{"path":"test.ts"}',
|
||||||
|
"file contents",
|
||||||
|
);
|
||||||
expect(ChatTurn.findById).toHaveBeenCalledWith(mockTurnId);
|
expect(ChatTurn.findById).toHaveBeenCalledWith(mockTurnId);
|
||||||
expect(mockTurn.toolCalls).toHaveLength(1);
|
expect(mockTurn.toolCalls).toHaveLength(1);
|
||||||
expect(mockTurn.toolCalls[0]).toEqual({
|
expect(mockTurn.toolCalls[0]).toEqual({
|
||||||
callId: 'call-123',
|
callId: "call-123",
|
||||||
name: 'readFile',
|
name: "readFile",
|
||||||
parameters: '{"path":"test.ts"}',
|
parameters: '{"path":"test.ts"}',
|
||||||
response: 'file contents',
|
response: "file contents",
|
||||||
});
|
});
|
||||||
expect(mockTurn.stats.toolCallCount).toBe(1);
|
expect(mockTurn.stats.toolCallCount).toBe(1);
|
||||||
expect(mockTurn.save).toHaveBeenCalled();
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log warning if no chat session is active', async () => {
|
it("should log warning if no chat session is active", async () => {
|
||||||
await droneSession.onToolCall('call-1', 'test', '{}', 'result');
|
await droneSession.onToolCall("call-1", "test", "{}", "result");
|
||||||
// No exception thrown, warning logged internally
|
// No exception thrown, warning logged internally
|
||||||
expect(mockSocket.emit).not.toHaveBeenCalled();
|
expect(mockSocket.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onWorkOrderComplete', () => {
|
describe("onWorkOrderComplete", () => {
|
||||||
it('should update ChatTurn status and emit to code session on success', async () => {
|
it("should update ChatTurn status and emit to code session on success", async () => {
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: { emit: vi.fn() },
|
socket: { emit: vi.fn() },
|
||||||
};
|
};
|
||||||
@ -175,56 +230,69 @@ describe('DroneSession', () => {
|
|||||||
status: ChatTurnStatus.Processing,
|
status: ChatTurnStatus.Processing,
|
||||||
save: vi.fn().mockResolvedValue(undefined),
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(mockCodeSession as any);
|
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||||
|
mockCodeSession as any,
|
||||||
|
);
|
||||||
vi.mocked(ChatTurn.findById).mockResolvedValue(mockTurn as any);
|
vi.mocked(ChatTurn.findById).mockResolvedValue(mockTurn as any);
|
||||||
|
|
||||||
droneSession.setChatSessionId(mockChatSessionId);
|
droneSession.setChatSessionId(mockChatSessionId);
|
||||||
|
|
||||||
await droneSession.onWorkOrderComplete(mockTurnId.toHexString(), true);
|
await droneSession.onWorkOrderComplete(mockTurnId, true);
|
||||||
|
|
||||||
expect(ChatTurn.findById).toHaveBeenCalledWith(mockTurnId.toHexString());
|
expect(ChatTurn.findById).toHaveBeenCalledWith(mockTurnId);
|
||||||
expect(mockTurn.status).toBe(ChatTurnStatus.Finished);
|
expect(mockTurn.status).toBe(ChatTurnStatus.Finished);
|
||||||
expect(mockTurn.save).toHaveBeenCalled();
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith('workOrderComplete', mockTurnId.toHexString(), true, undefined);
|
expect(mockCodeSession.socket.emit).toHaveBeenCalledWith(
|
||||||
|
"workOrderComplete",
|
||||||
|
mockTurnId,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
expect(droneSession.currentTurnId).toBeUndefined();
|
expect(droneSession.currentTurnId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update ChatTurn to Error status on failure', async () => {
|
it("should update ChatTurn to Error status on failure", async () => {
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: { emit: vi.fn() },
|
socket: { emit: vi.fn() },
|
||||||
};
|
};
|
||||||
const mockTurn = {
|
const mockTurn = {
|
||||||
status: ChatTurnStatus.Processing,
|
status: ChatTurnStatus.Processing,
|
||||||
response: '',
|
response: "",
|
||||||
save: vi.fn().mockResolvedValue(undefined),
|
save: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(mockCodeSession as any);
|
vi.mocked(SocketService.getCodeSessionByChatSessionId).mockReturnValue(
|
||||||
|
mockCodeSession as any,
|
||||||
|
);
|
||||||
vi.mocked(ChatTurn.findById).mockResolvedValue(mockTurn as any);
|
vi.mocked(ChatTurn.findById).mockResolvedValue(mockTurn as any);
|
||||||
|
|
||||||
droneSession.setChatSessionId(mockChatSessionId);
|
droneSession.setChatSessionId(mockChatSessionId);
|
||||||
|
|
||||||
await droneSession.onWorkOrderComplete(mockTurnId.toHexString(), false, 'Agent crashed');
|
await droneSession.onWorkOrderComplete(
|
||||||
|
mockTurnId,
|
||||||
|
false,
|
||||||
|
"Agent crashed",
|
||||||
|
);
|
||||||
|
|
||||||
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
expect(mockTurn.status).toBe(ChatTurnStatus.Error);
|
||||||
expect(mockTurn.response).toBe('Agent crashed');
|
expect(mockTurn.response).toBe("Agent crashed");
|
||||||
expect(mockTurn.save).toHaveBeenCalled();
|
expect(mockTurn.save).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log warning if no chat session is active', async () => {
|
it("should log warning if no chat session is active", async () => {
|
||||||
await droneSession.onWorkOrderComplete(mockTurnId.toHexString(), true);
|
await droneSession.onWorkOrderComplete(mockTurnId, true);
|
||||||
// No exception thrown, warning logged internally
|
// No exception thrown, warning logged internally
|
||||||
expect(mockSocket.emit).not.toHaveBeenCalled();
|
expect(mockSocket.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onRequestTermination', () => {
|
describe("onRequestTermination", () => {
|
||||||
it('should forward requestTermination to drone socket with logging', async () => {
|
it("should forward requestTermination to drone socket with logging", async () => {
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = vi.fn();
|
||||||
const mockDroneCallback = vi.fn();
|
const mockDroneCallback = vi.fn();
|
||||||
mockSocket.emit.mockImplementation((event: string, ...args: any[]) => {
|
mockSocket.emit.mockImplementation((event: string, ...args: any[]) => {
|
||||||
if (event === 'requestTermination' && args.length > 0) {
|
if (event === "requestTermination" && args.length > 0) {
|
||||||
const cb = args[args.length - 1];
|
const cb = args[args.length - 1];
|
||||||
if (typeof cb === 'function') {
|
if (typeof cb === "function") {
|
||||||
cb(true);
|
cb(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,16 +300,19 @@ describe('DroneSession', () => {
|
|||||||
|
|
||||||
await droneSession.onRequestTermination(mockCallback);
|
await droneSession.onRequestTermination(mockCallback);
|
||||||
|
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('requestTermination', expect.any(Function));
|
expect(mockSocket.emit).toHaveBeenCalledWith(
|
||||||
|
"requestTermination",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
expect(mockCallback).toHaveBeenCalledWith(true);
|
expect(mockCallback).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass through failure response from drone', async () => {
|
it("should pass through failure response from drone", async () => {
|
||||||
const mockCallback = vi.fn();
|
const mockCallback = vi.fn();
|
||||||
mockSocket.emit.mockImplementation((event: string, ...args: any[]) => {
|
mockSocket.emit.mockImplementation((event: string, ...args: any[]) => {
|
||||||
if (event === 'requestTermination' && args.length > 0) {
|
if (event === "requestTermination" && args.length > 0) {
|
||||||
const cb = args[args.length - 1];
|
const cb = args[args.length - 1];
|
||||||
if (typeof cb === 'function') {
|
if (typeof cb === "function") {
|
||||||
cb(false);
|
cb(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -249,7 +320,10 @@ describe('DroneSession', () => {
|
|||||||
|
|
||||||
await droneSession.onRequestTermination(mockCallback);
|
await droneSession.onRequestTermination(mockCallback);
|
||||||
|
|
||||||
expect(mockSocket.emit).toHaveBeenCalledWith('requestTermination', expect.any(Function));
|
expect(mockSocket.emit).toHaveBeenCalledWith(
|
||||||
|
"requestTermination",
|
||||||
|
expect.any(Function),
|
||||||
|
);
|
||||||
expect(mockCallback).toHaveBeenCalledWith(false);
|
expect(mockCallback).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { vi } from 'vitest';
|
import { vi } from "vitest";
|
||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
IDroneRegistration,
|
IDroneRegistration,
|
||||||
@ -11,20 +11,22 @@ import {
|
|||||||
IProject,
|
IProject,
|
||||||
DroneStatus,
|
DroneStatus,
|
||||||
ChatSessionMode,
|
ChatSessionMode,
|
||||||
} from '@gadget/api';
|
} from "@gadget/api";
|
||||||
|
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a mock socket object with common methods stubbed.
|
* Creates a mock socket object with common methods stubbed.
|
||||||
*/
|
*/
|
||||||
export function createMockSocket(id?: string) {
|
export function createMockSocket(id?: string) {
|
||||||
return {
|
return {
|
||||||
id: id || `socket-${new Types.ObjectId().toHexString()}`,
|
id: id || `socket-${nanoid()}`,
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
emit: vi.fn(),
|
emit: vi.fn(),
|
||||||
disconnect: vi.fn(),
|
disconnect: vi.fn(),
|
||||||
data: {},
|
data: {},
|
||||||
handshake: {
|
handshake: {
|
||||||
auth: { token: '' },
|
auth: { token: "" },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -34,9 +36,9 @@ export function createMockSocket(id?: string) {
|
|||||||
*/
|
*/
|
||||||
export function createMockUser(overrides?: Partial<IUser>): IUser {
|
export function createMockUser(overrides?: Partial<IUser>): IUser {
|
||||||
return {
|
return {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
email: `user-${new Types.ObjectId().toHexString()}@example.com`,
|
email: `user-${nanoid()}@example.com`,
|
||||||
displayName: 'Test User',
|
displayName: "Test User",
|
||||||
banned: false,
|
banned: false,
|
||||||
admin: false,
|
admin: false,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -50,16 +52,16 @@ export function createMockUser(overrides?: Partial<IUser>): IUser {
|
|||||||
*/
|
*/
|
||||||
export function createMockDroneRegistration(
|
export function createMockDroneRegistration(
|
||||||
user?: IUser,
|
user?: IUser,
|
||||||
overrides?: Partial<IDroneRegistration>
|
overrides?: Partial<IDroneRegistration>,
|
||||||
): IDroneRegistration {
|
): IDroneRegistration {
|
||||||
const testUser = user || createMockUser();
|
const testUser = user || createMockUser();
|
||||||
return {
|
return {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
user: testUser,
|
user: testUser,
|
||||||
hostname: `drone-${new Types.ObjectId().toHexString()}`,
|
hostname: `drone-${nanoid()}`,
|
||||||
workspaceDir: '/test/workspace',
|
workspaceDir: "/test/workspace",
|
||||||
status: DroneStatus.Available,
|
status: DroneStatus.Available,
|
||||||
...overrides,
|
...overrides,
|
||||||
} as IDroneRegistration;
|
} as IDroneRegistration;
|
||||||
@ -71,18 +73,18 @@ export function createMockDroneRegistration(
|
|||||||
export function createMockChatSession(
|
export function createMockChatSession(
|
||||||
user?: IUser,
|
user?: IUser,
|
||||||
project?: IProject,
|
project?: IProject,
|
||||||
overrides?: Partial<IChatSession>
|
overrides?: Partial<IChatSession>,
|
||||||
): IChatSession {
|
): IChatSession {
|
||||||
return {
|
return {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
user: user?._id || new Types.ObjectId(),
|
user: user?._id || nanoid(),
|
||||||
project: project?._id || new Types.ObjectId(),
|
project: project?._id || nanoid(),
|
||||||
name: 'Test Chat Session',
|
name: "Test Chat Session",
|
||||||
mode: ChatSessionMode.Build,
|
mode: ChatSessionMode.Build,
|
||||||
provider: new Types.ObjectId(),
|
provider: nanoid(),
|
||||||
selectedModel: 'llama3.2',
|
selectedModel: "llama3.2",
|
||||||
stats: {
|
stats: {
|
||||||
toolCallCount: 0,
|
toolCallCount: 0,
|
||||||
fileOpCount: 0,
|
fileOpCount: 0,
|
||||||
@ -97,16 +99,16 @@ export function createMockChatSession(
|
|||||||
*/
|
*/
|
||||||
export function createMockProject(
|
export function createMockProject(
|
||||||
user?: IUser,
|
user?: IUser,
|
||||||
overrides?: Partial<IProject>
|
overrides?: Partial<IProject>,
|
||||||
): IProject {
|
): IProject {
|
||||||
return {
|
return {
|
||||||
_id: new Types.ObjectId(),
|
_id: nanoid(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
user: user?._id || new Types.ObjectId(),
|
user: user?._id || nanoid(),
|
||||||
slug: `project-${new Types.ObjectId().toHexString()}`,
|
slug: `project-${nanoid()}`,
|
||||||
name: 'Test Project',
|
name: "Test Project",
|
||||||
gitUrl: 'https://github.com/test/test.git',
|
gitUrl: "https://github.com/test/test.git",
|
||||||
...overrides,
|
...overrides,
|
||||||
} as IProject;
|
} as IProject;
|
||||||
}
|
}
|
||||||
@ -132,5 +134,5 @@ export function captureSocketEmits(socket: any): EmitCall[] {
|
|||||||
*/
|
*/
|
||||||
export function extractCallback(emitCall: EmitCall): Function | null {
|
export function extractCallback(emitCall: EmitCall): Function | null {
|
||||||
const lastArg = emitCall.args[emitCall.args.length - 1];
|
const lastArg = emitCall.args[emitCall.args.length - 1];
|
||||||
return typeof lastArg === 'function' ? lastArg : null;
|
return typeof lastArg === "function" ? lastArg : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,20 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import { DroneStatus } from '@gadget/api';
|
import { DroneStatus } from "@gadget/api";
|
||||||
import {
|
import {
|
||||||
createMockSocket,
|
createMockSocket,
|
||||||
createMockUser,
|
createMockUser,
|
||||||
createMockDroneRegistration,
|
createMockDroneRegistration,
|
||||||
createMockChatSession,
|
createMockChatSession,
|
||||||
createMockProject,
|
createMockProject,
|
||||||
} from './fixtures';
|
} from "./fixtures";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
// Mock the entire socket service module
|
// Mock the entire socket service module
|
||||||
vi.mock('../src/services/socket', async () => {
|
vi.mock("../src/services/socket", async () => {
|
||||||
const chatSessionIndex = new Map();
|
const chatSessionIndex = new Map();
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
@ -36,7 +37,7 @@ vi.mock('../src/services/socket', async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SocketService Session Indexing', () => {
|
describe("SocketService Session Indexing", () => {
|
||||||
let SocketService: any;
|
let SocketService: any;
|
||||||
let mockUser: any;
|
let mockUser: any;
|
||||||
let mockDrone: any;
|
let mockDrone: any;
|
||||||
@ -46,7 +47,7 @@ describe('SocketService Session Indexing', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
const mod = await import('../src/services/socket');
|
const mod = await import("../src/services/socket");
|
||||||
SocketService = mod.default;
|
SocketService = mod.default;
|
||||||
|
|
||||||
// Clear all session maps
|
// Clear all session maps
|
||||||
@ -62,47 +63,47 @@ describe('SocketService Session Indexing', () => {
|
|||||||
mockProject = createMockProject(mockUser);
|
mockProject = createMockProject(mockUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Drone Session Indexing', () => {
|
describe("Drone Session Indexing", () => {
|
||||||
it('should store drone session by socket.id and registration._id', () => {
|
it("should store drone session by socket.id and registration._id", () => {
|
||||||
const mockSocket = createMockSocket('drone-socket-123');
|
const mockSocket = createMockSocket("drone-socket-123");
|
||||||
const mockDroneSession = {
|
const mockDroneSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
registration: mockDrone,
|
registration: mockDrone,
|
||||||
type: 'drone',
|
type: "drone",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in both indexes
|
// Store in both indexes
|
||||||
SocketService.droneSessions.set(mockSocket.id, mockDroneSession);
|
SocketService.droneSessions.set(mockSocket.id, mockDroneSession);
|
||||||
SocketService.droneRegistrationIndex.set(
|
SocketService.droneRegistrationIndex.set(mockDrone._id, mockDroneSession);
|
||||||
mockDrone._id.toHexString(),
|
|
||||||
mockDroneSession
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify both lookups work
|
// Verify both lookups work
|
||||||
expect(SocketService.droneSessions.get(mockSocket.id)).toBe(mockDroneSession);
|
expect(SocketService.droneSessions.get(mockSocket.id)).toBe(
|
||||||
expect(SocketService.droneRegistrationIndex.get(mockDrone._id.toHexString())).toBe(mockDroneSession);
|
mockDroneSession,
|
||||||
|
);
|
||||||
|
expect(SocketService.droneRegistrationIndex.get(mockDrone._id)).toBe(
|
||||||
|
mockDroneSession,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find drone session by registration._id', () => {
|
it("should find drone session by registration._id", () => {
|
||||||
const mockSocket = createMockSocket('drone-socket-456');
|
const mockSocket = createMockSocket("drone-socket-456");
|
||||||
const mockDroneSession = {
|
const mockDroneSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
registration: mockDrone,
|
registration: mockDrone,
|
||||||
type: 'drone',
|
type: "drone",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in both indexes
|
// Store in both indexes
|
||||||
SocketService.droneSessions.set(mockSocket.id, mockDroneSession);
|
SocketService.droneSessions.set(mockSocket.id, mockDroneSession);
|
||||||
SocketService.droneRegistrationIndex.set(
|
SocketService.droneRegistrationIndex.set(mockDrone._id, mockDroneSession);
|
||||||
mockDrone._id.toHexString(),
|
|
||||||
mockDroneSession
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mock getDroneSession to use the registration index
|
// Mock getDroneSession to use the registration index
|
||||||
SocketService.getDroneSession.mockImplementation((registration: any) => {
|
SocketService.getDroneSession.mockImplementation((registration: any) => {
|
||||||
const session = SocketService.droneRegistrationIndex.get(registration._id.toHexString());
|
const session = SocketService.droneRegistrationIndex.get(
|
||||||
|
registration._id,
|
||||||
|
);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const error = new Error('drone session not found');
|
const error = new Error("drone session not found");
|
||||||
(error as any).statusCode = 404;
|
(error as any).statusCode = 404;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -113,87 +114,90 @@ describe('SocketService Session Indexing', () => {
|
|||||||
expect(found).toBe(mockDroneSession);
|
expect(found).toBe(mockDroneSession);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw 404 when drone session not found', () => {
|
it("should throw 404 when drone session not found", () => {
|
||||||
const nonExistentDrone = createMockDroneRegistration(mockUser);
|
const nonExistentDrone = createMockDroneRegistration(mockUser);
|
||||||
|
|
||||||
SocketService.getDroneSession.mockImplementation(() => {
|
SocketService.getDroneSession.mockImplementation(() => {
|
||||||
const error = new Error('drone session not found');
|
const error = new Error("drone session not found");
|
||||||
(error as any).statusCode = 404;
|
(error as any).statusCode = 404;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => SocketService.getDroneSession(nonExistentDrone)).toThrow('drone session not found');
|
expect(() => SocketService.getDroneSession(nonExistentDrone)).toThrow(
|
||||||
expect(() => SocketService.getDroneSession(nonExistentDrone)).toThrowError(expect.objectContaining({
|
"drone session not found",
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
SocketService.getDroneSession(nonExistentDrone),
|
||||||
|
).toThrowError(
|
||||||
|
expect.objectContaining({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove drone session from all indexes on disconnect', () => {
|
it("should remove drone session from all indexes on disconnect", () => {
|
||||||
const mockSocket = createMockSocket('drone-socket-789');
|
const mockSocket = createMockSocket("drone-socket-789");
|
||||||
const mockDroneSession = {
|
const mockDroneSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
registration: mockDrone,
|
registration: mockDrone,
|
||||||
type: 'drone',
|
type: "drone",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in indexes
|
// Store in indexes
|
||||||
SocketService.droneSessions.set(mockSocket.id, mockDroneSession);
|
SocketService.droneSessions.set(mockSocket.id, mockDroneSession);
|
||||||
SocketService.droneRegistrationIndex.set(
|
SocketService.droneRegistrationIndex.set(mockDrone._id, mockDroneSession);
|
||||||
mockDrone._id.toHexString(),
|
|
||||||
mockDroneSession
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate disconnect
|
// Simulate disconnect
|
||||||
SocketService.droneSessions.delete(mockSocket.id);
|
SocketService.droneSessions.delete(mockSocket.id);
|
||||||
SocketService.droneRegistrationIndex.delete(mockDrone._id.toHexString());
|
SocketService.droneRegistrationIndex.delete(mockDrone._id);
|
||||||
|
|
||||||
// Verify removal from all indexes
|
// Verify removal from all indexes
|
||||||
expect(SocketService.droneSessions.get(mockSocket.id)).toBeUndefined();
|
expect(SocketService.droneSessions.get(mockSocket.id)).toBeUndefined();
|
||||||
expect(SocketService.droneRegistrationIndex.get(mockDrone._id.toHexString())).toBeUndefined();
|
expect(
|
||||||
|
SocketService.droneRegistrationIndex.get(mockDrone._id),
|
||||||
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Code Session Indexing', () => {
|
describe("Code Session Indexing", () => {
|
||||||
it('should store code session by socket.id and user._id', () => {
|
it("should store code session by socket.id and user._id", () => {
|
||||||
const mockSocket = createMockSocket('code-socket-123');
|
const mockSocket = createMockSocket("code-socket-123");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
type: 'code',
|
type: "code",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in both indexes
|
// Store in both indexes
|
||||||
SocketService.codeSessions.set(mockSocket.id, mockCodeSession);
|
SocketService.codeSessions.set(mockSocket.id, mockCodeSession);
|
||||||
SocketService.codeSessionUserIndex.set(
|
SocketService.codeSessionUserIndex.set(mockUser._id, mockCodeSession);
|
||||||
mockUser._id.toHexString(),
|
|
||||||
mockCodeSession
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify both lookups work
|
// Verify both lookups work
|
||||||
expect(SocketService.codeSessions.get(mockSocket.id)).toBe(mockCodeSession);
|
expect(SocketService.codeSessions.get(mockSocket.id)).toBe(
|
||||||
expect(SocketService.codeSessionUserIndex.get(mockUser._id.toHexString())).toBe(mockCodeSession);
|
mockCodeSession,
|
||||||
|
);
|
||||||
|
expect(SocketService.codeSessionUserIndex.get(mockUser._id)).toBe(
|
||||||
|
mockCodeSession,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should find code session by user._id', () => {
|
it("should find code session by user._id", () => {
|
||||||
const mockSocket = createMockSocket('code-socket-456');
|
const mockSocket = createMockSocket("code-socket-456");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
type: 'code',
|
type: "code",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in both indexes
|
// Store in both indexes
|
||||||
SocketService.codeSessions.set(mockSocket.id, mockCodeSession);
|
SocketService.codeSessions.set(mockSocket.id, mockCodeSession);
|
||||||
SocketService.codeSessionUserIndex.set(
|
SocketService.codeSessionUserIndex.set(mockUser._id, mockCodeSession);
|
||||||
mockUser._id.toHexString(),
|
|
||||||
mockCodeSession
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mock getCodeSession to use the user index
|
// Mock getCodeSession to use the user index
|
||||||
SocketService.getCodeSession.mockImplementation((user: any) => {
|
SocketService.getCodeSession.mockImplementation((user: any) => {
|
||||||
const session = SocketService.codeSessionUserIndex.get(user._id.toHexString());
|
const session = SocketService.codeSessionUserIndex.get(user._id);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
const error = new Error('code session not found');
|
const error = new Error("code session not found");
|
||||||
(error as any).statusCode = 404;
|
(error as any).statusCode = 404;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -204,114 +208,138 @@ describe('SocketService Session Indexing', () => {
|
|||||||
expect(found).toBe(mockCodeSession);
|
expect(found).toBe(mockCodeSession);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw 404 when code session not found', () => {
|
it("should throw 404 when code session not found", () => {
|
||||||
const nonExistentUser = createMockUser();
|
const nonExistentUser = createMockUser();
|
||||||
|
|
||||||
SocketService.getCodeSession.mockImplementation(() => {
|
SocketService.getCodeSession.mockImplementation(() => {
|
||||||
const error = new Error('code session not found');
|
const error = new Error("code session not found");
|
||||||
(error as any).statusCode = 404;
|
(error as any).statusCode = 404;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => SocketService.getCodeSession(nonExistentUser)).toThrow('code session not found');
|
expect(() => SocketService.getCodeSession(nonExistentUser)).toThrow(
|
||||||
expect(() => SocketService.getCodeSession(nonExistentUser)).toThrowError(expect.objectContaining({
|
"code session not found",
|
||||||
|
);
|
||||||
|
expect(() => SocketService.getCodeSession(nonExistentUser)).toThrowError(
|
||||||
|
expect.objectContaining({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove code session from all indexes on disconnect', () => {
|
it("should remove code session from all indexes on disconnect", () => {
|
||||||
const mockSocket = createMockSocket('code-socket-789');
|
const mockSocket = createMockSocket("code-socket-789");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
type: 'code',
|
type: "code",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in indexes
|
// Store in indexes
|
||||||
SocketService.codeSessions.set(mockSocket.id, mockCodeSession);
|
SocketService.codeSessions.set(mockSocket.id, mockCodeSession);
|
||||||
SocketService.codeSessionUserIndex.set(
|
SocketService.codeSessionUserIndex.set(mockUser._id, mockCodeSession);
|
||||||
mockUser._id.toHexString(),
|
|
||||||
mockCodeSession
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate disconnect
|
// Simulate disconnect
|
||||||
SocketService.codeSessions.delete(mockSocket.id);
|
SocketService.codeSessions.delete(mockSocket.id);
|
||||||
SocketService.codeSessionUserIndex.delete(mockUser._id.toHexString());
|
SocketService.codeSessionUserIndex.delete(mockUser._id);
|
||||||
|
|
||||||
// Verify removal from all indexes
|
// Verify removal from all indexes
|
||||||
expect(SocketService.codeSessions.get(mockSocket.id)).toBeUndefined();
|
expect(SocketService.codeSessions.get(mockSocket.id)).toBeUndefined();
|
||||||
expect(SocketService.codeSessionUserIndex.get(mockUser._id.toHexString())).toBeUndefined();
|
expect(
|
||||||
|
SocketService.codeSessionUserIndex.get(mockUser._id),
|
||||||
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Chat Session Index', () => {
|
describe("Chat Session Index", () => {
|
||||||
it('should register and retrieve code session by chatSessionId', () => {
|
it("should register and retrieve code session by chatSessionId", () => {
|
||||||
const mockSocket = createMockSocket('code-socket-chat');
|
const mockSocket = createMockSocket("code-socket-chat");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
type: 'code',
|
type: "code",
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatSessionId = mockChatSession._id.toHexString();
|
const chatSessionId = mockChatSession._id;
|
||||||
|
|
||||||
// Mock the retrieval to return our session after registration
|
// Mock the retrieval to return our session after registration
|
||||||
SocketService.registerChatSession(chatSessionId, mockCodeSession);
|
SocketService.registerChatSession(chatSessionId, mockCodeSession);
|
||||||
SocketService.getCodeSessionByChatSessionId.mockReturnValue(mockCodeSession);
|
SocketService.getCodeSessionByChatSessionId.mockReturnValue(
|
||||||
|
mockCodeSession,
|
||||||
|
);
|
||||||
|
|
||||||
const found = SocketService.getCodeSessionByChatSessionId(mockChatSession._id);
|
const found = SocketService.getCodeSessionByChatSessionId(
|
||||||
|
mockChatSession._id,
|
||||||
|
);
|
||||||
expect(found).toBe(mockCodeSession);
|
expect(found).toBe(mockCodeSession);
|
||||||
expect(SocketService.registerChatSession).toHaveBeenCalledWith(chatSessionId, mockCodeSession);
|
expect(SocketService.registerChatSession).toHaveBeenCalledWith(
|
||||||
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(mockChatSession._id);
|
chatSessionId,
|
||||||
|
mockCodeSession,
|
||||||
|
);
|
||||||
|
expect(SocketService.getCodeSessionByChatSessionId).toHaveBeenCalledWith(
|
||||||
|
mockChatSession._id,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle chatSessionId as string or ObjectId', () => {
|
it("should handle chatSessionId as string or ObjectId", () => {
|
||||||
const mockSocket = createMockSocket('code-socket-chat2');
|
const mockSocket = createMockSocket("code-socket-chat2");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
type: 'code',
|
type: "code",
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatSessionId = mockChatSession._id.toHexString();
|
const chatSessionId = mockChatSession._id;
|
||||||
SocketService.registerChatSession(chatSessionId, mockCodeSession);
|
SocketService.registerChatSession(chatSessionId, mockCodeSession);
|
||||||
SocketService.getCodeSessionByChatSessionId.mockReturnValue(mockCodeSession);
|
SocketService.getCodeSessionByChatSessionId.mockReturnValue(
|
||||||
|
mockCodeSession,
|
||||||
|
);
|
||||||
|
|
||||||
// Test with string
|
// Test with string
|
||||||
const found1 = SocketService.getCodeSessionByChatSessionId(chatSessionId);
|
const found1 = SocketService.getCodeSessionByChatSessionId(chatSessionId);
|
||||||
expect(found1).toBe(mockCodeSession);
|
expect(found1).toBe(mockCodeSession);
|
||||||
|
|
||||||
// Test with ObjectId
|
// Test with ObjectId
|
||||||
const found2 = SocketService.getCodeSessionByChatSessionId(mockChatSession._id);
|
const found2 = SocketService.getCodeSessionByChatSessionId(
|
||||||
|
mockChatSession._id,
|
||||||
|
);
|
||||||
expect(found2).toBe(mockCodeSession);
|
expect(found2).toBe(mockCodeSession);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw 404 when chat session not found', () => {
|
it("should throw 404 when chat session not found", () => {
|
||||||
const nonExistentChatSessionId = new Types.ObjectId();
|
const nonExistentChatSessionId = nanoid();
|
||||||
|
|
||||||
SocketService.getCodeSessionByChatSessionId.mockImplementation(() => {
|
SocketService.getCodeSessionByChatSessionId.mockImplementation(() => {
|
||||||
const error = new Error('code session not found for chat session');
|
const error = new Error("code session not found for chat session");
|
||||||
(error as any).statusCode = 404;
|
(error as any).statusCode = 404;
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => SocketService.getCodeSessionByChatSessionId(nonExistentChatSessionId)).toThrow('code session not found for chat session');
|
expect(() =>
|
||||||
expect(() => SocketService.getCodeSessionByChatSessionId(nonExistentChatSessionId)).toThrowError(expect.objectContaining({
|
SocketService.getCodeSessionByChatSessionId(nonExistentChatSessionId),
|
||||||
|
).toThrow("code session not found for chat session");
|
||||||
|
expect(() =>
|
||||||
|
SocketService.getCodeSessionByChatSessionId(nonExistentChatSessionId),
|
||||||
|
).toThrowError(
|
||||||
|
expect.objectContaining({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unregister chat session', () => {
|
it("should unregister chat session", () => {
|
||||||
const mockSocket = createMockSocket('code-socket-chat3');
|
const mockSocket = createMockSocket("code-socket-chat3");
|
||||||
const mockCodeSession = {
|
const mockCodeSession = {
|
||||||
socket: mockSocket,
|
socket: mockSocket,
|
||||||
user: mockUser,
|
user: mockUser,
|
||||||
type: 'code',
|
type: "code",
|
||||||
};
|
};
|
||||||
|
|
||||||
const chatSessionId = mockChatSession._id.toHexString();
|
const chatSessionId = mockChatSession._id;
|
||||||
SocketService.registerChatSession(chatSessionId, mockCodeSession);
|
SocketService.registerChatSession(chatSessionId, mockCodeSession);
|
||||||
SocketService.getCodeSessionByChatSessionId.mockReturnValue(mockCodeSession);
|
SocketService.getCodeSessionByChatSessionId.mockReturnValue(
|
||||||
|
mockCodeSession,
|
||||||
|
);
|
||||||
|
|
||||||
// Verify it's registered
|
// Verify it's registered
|
||||||
const found = SocketService.getCodeSessionByChatSessionId(chatSessionId);
|
const found = SocketService.getCodeSessionByChatSessionId(chatSessionId);
|
||||||
@ -319,7 +347,9 @@ describe('SocketService Session Indexing', () => {
|
|||||||
|
|
||||||
// Unregister
|
// Unregister
|
||||||
SocketService.unregisterChatSession(chatSessionId);
|
SocketService.unregisterChatSession(chatSessionId);
|
||||||
expect(SocketService.unregisterChatSession).toHaveBeenCalledWith(chatSessionId);
|
expect(SocketService.unregisterChatSession).toHaveBeenCalledWith(
|
||||||
|
chatSessionId,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import {
|
|||||||
RequestSessionLockCallback,
|
RequestSessionLockCallback,
|
||||||
RequestWorkspaceModeCallback,
|
RequestWorkspaceModeCallback,
|
||||||
ServerToClientEvents,
|
ServerToClientEvents,
|
||||||
Types,
|
|
||||||
WorkspaceMode,
|
WorkspaceMode,
|
||||||
} from "@gadget/api";
|
} from "@gadget/api";
|
||||||
|
|
||||||
@ -96,7 +95,7 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
|
|
||||||
// Update workspace with registration
|
// Update workspace with registration
|
||||||
WorkspaceService.updateRegistration({
|
WorkspaceService.updateRegistration({
|
||||||
_id: this.registration._id.toHexString(),
|
_id: this.registration._id,
|
||||||
status: DroneStatus.Starting,
|
status: DroneStatus.Starting,
|
||||||
});
|
});
|
||||||
await WorkspaceService.writeWorkspaceData();
|
await WorkspaceService.writeWorkspaceData();
|
||||||
@ -118,7 +117,7 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
|
|
||||||
await PlatformService.setStatus(DroneStatus.Available);
|
await PlatformService.setStatus(DroneStatus.Available);
|
||||||
WorkspaceService.updateRegistration({
|
WorkspaceService.updateRegistration({
|
||||||
_id: this.registration._id.toHexString(),
|
_id: this.registration._id,
|
||||||
status: DroneStatus.Available,
|
status: DroneStatus.Available,
|
||||||
});
|
});
|
||||||
await WorkspaceService.writeWorkspaceData();
|
await WorkspaceService.writeWorkspaceData();
|
||||||
@ -219,25 +218,6 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
chatSession: IChatSession,
|
chatSession: IChatSession,
|
||||||
cb: RequestSessionLockCallback,
|
cb: RequestSessionLockCallback,
|
||||||
) {
|
) {
|
||||||
/*
|
|
||||||
* Convert the IDs we'll actually use into ObjectId instances as expected in
|
|
||||||
* the interfaces.
|
|
||||||
*/
|
|
||||||
|
|
||||||
registration._id = Types.ObjectId.createFromHexString(
|
|
||||||
registration._id as unknown as string,
|
|
||||||
);
|
|
||||||
project._id = Types.ObjectId.createFromHexString(
|
|
||||||
project._id as unknown as string,
|
|
||||||
);
|
|
||||||
chatSession._id = Types.ObjectId.createFromHexString(
|
|
||||||
chatSession._id as unknown as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Process the request
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.log.info("requestSessionLock received", {
|
this.log.info("requestSessionLock received", {
|
||||||
registration,
|
registration,
|
||||||
project,
|
project,
|
||||||
@ -249,19 +229,19 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
);
|
);
|
||||||
return cb(false, "not registered");
|
return cb(false, "not registered");
|
||||||
}
|
}
|
||||||
if (!registration._id.equals(this.registration._id)) {
|
if (registration._id !== this.registration._id) {
|
||||||
this.log.warn(
|
this.log.warn(
|
||||||
"received session lock request for a different drone registration",
|
"received session lock request for a different drone registration",
|
||||||
{
|
{
|
||||||
myId: this.registration._id.toHexString(),
|
myId: this.registration._id,
|
||||||
requestId: registration._id.toHexString(),
|
requestId: registration._id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return cb(false, "invalid registration");
|
return cb(false, "invalid registration");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.workspaceMode = WorkspaceMode.User;
|
this.workspaceMode = WorkspaceMode.User;
|
||||||
cb(true, chatSession._id.toHexString());
|
cb(true, chatSession._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onRequestWorkspaceMode(
|
async onRequestWorkspaceMode(
|
||||||
@ -311,9 +291,9 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
// Write work order cache BEFORE processing (for crash recovery)
|
// Write work order cache BEFORE processing (for crash recovery)
|
||||||
try {
|
try {
|
||||||
await WorkspaceService.writeWorkOrderCache(
|
await WorkspaceService.writeWorkOrderCache(
|
||||||
turn._id.toHexString(),
|
turn._id,
|
||||||
chatSession._id.toHexString(),
|
chatSession._id,
|
||||||
project._id.toHexString(),
|
project._id,
|
||||||
turn.prompts.user,
|
turn.prompts.user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -372,13 +352,13 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
|
|
||||||
async getUserCredentials(): Promise<UserCredentials> {
|
async getUserCredentials(): Promise<UserCredentials> {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const userArg = args.find(a => a.startsWith('--user='));
|
const userArg = args.find((a) => a.startsWith("--user="));
|
||||||
const passArg = args.find(a => a.startsWith('--password='));
|
const passArg = args.find((a) => a.startsWith("--password="));
|
||||||
|
|
||||||
if (userArg && passArg) {
|
if (userArg && passArg) {
|
||||||
return {
|
return {
|
||||||
email: userArg.split('=')[1],
|
email: userArg.split("=")[1],
|
||||||
password: passArg.split('=')[1],
|
password: passArg.split("=")[1],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,7 @@
|
|||||||
|
|
||||||
import { Types } from "@gadget/api";
|
import { Types } from "@gadget/api";
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import {
|
import { IAiChatOptions, type IContextChatMessage } from "@gadget/ai";
|
||||||
IAiChatOptions,
|
|
||||||
type IContextChatMessage,
|
|
||||||
} from "@gadget/ai";
|
|
||||||
import {
|
import {
|
||||||
IChatSession,
|
IChatSession,
|
||||||
IChatTurn,
|
IChatTurn,
|
||||||
@ -121,15 +118,13 @@ class AgentService extends GadgetService {
|
|||||||
} while (keepProcessing);
|
} while (keepProcessing);
|
||||||
|
|
||||||
// Emit work order complete
|
// Emit work order complete
|
||||||
socket.emit("workOrderComplete", turn._id.toHexString(), true);
|
socket.emit("workOrderComplete", turn._id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
|
buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
|
||||||
const session = workOrder.turn.session;
|
const session = workOrder.turn.session as IChatSession;
|
||||||
if (session instanceof Types.ObjectId || !session.user) {
|
if (!session.user) {
|
||||||
throw new Error(
|
throw new Error("ChatSession must be populated with user data");
|
||||||
"ChatSession must be populated with user data",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const user: IUser = session.user as IUser;
|
const user: IUser = session.user as IUser;
|
||||||
const messages: IContextChatMessage[] = [];
|
const messages: IContextChatMessage[] = [];
|
||||||
@ -143,7 +138,7 @@ class AgentService extends GadgetService {
|
|||||||
role: "user",
|
role: "user",
|
||||||
content: turn.prompts.user,
|
content: turn.prompts.user,
|
||||||
user: {
|
user: {
|
||||||
_id: user._id.toHexString(),
|
_id: user._id,
|
||||||
username: user.email,
|
username: user.email,
|
||||||
displayName: user.displayName,
|
displayName: user.displayName,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Types, IAiProvider as DbAiProvider } from "@gadget/api";
|
import { IAiProvider as DbAiProvider } from "@gadget/api";
|
||||||
import { GadgetService } from "../lib/service.ts";
|
import { GadgetService } from "../lib/service.ts";
|
||||||
import {
|
import {
|
||||||
type IAiChatOptions,
|
type IAiChatOptions,
|
||||||
@ -14,12 +14,13 @@ import {
|
|||||||
type IAiResponseStreamFn,
|
type IAiResponseStreamFn,
|
||||||
createAiApi,
|
createAiApi,
|
||||||
} from "@gadget/ai";
|
} from "@gadget/ai";
|
||||||
|
import { GadgetId } from "../../../packages/api/dist/lib/gadget-id.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drone-specific model config that accepts the database provider type.
|
* Drone-specific model config that accepts the database provider type.
|
||||||
*/
|
*/
|
||||||
export interface IDroneModelConfig {
|
export interface IDroneModelConfig {
|
||||||
provider: DbAiProvider | Types.ObjectId;
|
provider: DbAiProvider | GadgetId;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
params: {
|
params: {
|
||||||
reasoning: boolean;
|
reasoning: boolean;
|
||||||
@ -60,16 +61,12 @@ class AiService extends GadgetService {
|
|||||||
* The DB model uses `apiType` and extends Mongoose Document, while the runtime
|
* The DB model uses `apiType` and extends Mongoose Document, while the runtime
|
||||||
* config uses `sdk` and is a plain object.
|
* config uses `sdk` and is a plain object.
|
||||||
*/
|
*/
|
||||||
mapDbProviderToConfig(
|
mapDbProviderToConfig(provider: DbAiProvider | GadgetId): AiProviderConfig {
|
||||||
provider: DbAiProvider | Types.ObjectId,
|
if (typeof provider === "string") {
|
||||||
): AiProviderConfig {
|
throw new Error("Provider must be populated, not a GadgetId reference");
|
||||||
if (provider instanceof Types.ObjectId) {
|
|
||||||
throw new Error(
|
|
||||||
"Provider must be populated, not an ObjectId reference",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
_id: provider._id.toHexString(),
|
_id: provider._id,
|
||||||
name: provider.name,
|
name: provider.name,
|
||||||
sdk: provider.apiType, // map apiType → sdk
|
sdk: provider.apiType, // map apiType → sdk
|
||||||
baseUrl: provider.baseUrl,
|
baseUrl: provider.baseUrl,
|
||||||
@ -81,7 +78,7 @@ class AiService extends GadgetService {
|
|||||||
* Query the list of models available from the provider, then queries the
|
* Query the list of models available from the provider, then queries the
|
||||||
* models for their individual capabilities. The results are cached in the Gadget
|
* models for their individual capabilities. The results are cached in the Gadget
|
||||||
*/
|
*/
|
||||||
async discovery(provider: DbAiProvider | Types.ObjectId): Promise<void> {
|
async discovery(provider: DbAiProvider | GadgetId): Promise<void> {
|
||||||
const config = this.mapDbProviderToConfig(provider);
|
const config = this.mapDbProviderToConfig(provider);
|
||||||
this.log.info("discovering provider model list", {
|
this.log.info("discovering provider model list", {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
@ -93,7 +90,7 @@ class AiService extends GadgetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generate(
|
async generate(
|
||||||
provider: DbAiProvider | Types.ObjectId,
|
provider: DbAiProvider | GadgetId,
|
||||||
model: Omit<IAiModelConfig, "provider">,
|
model: Omit<IAiModelConfig, "provider">,
|
||||||
options: IAiGenerateOptions,
|
options: IAiGenerateOptions,
|
||||||
streamCallback?: IAiResponseStreamFn,
|
streamCallback?: IAiResponseStreamFn,
|
||||||
@ -111,7 +108,7 @@ class AiService extends GadgetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async chat(
|
async chat(
|
||||||
provider: DbAiProvider | Types.ObjectId,
|
provider: DbAiProvider | GadgetId,
|
||||||
model: Omit<IAiModelConfig, "provider">,
|
model: Omit<IAiModelConfig, "provider">,
|
||||||
options: IAiChatOptions,
|
options: IAiChatOptions,
|
||||||
streamCallback?: IAiResponseStreamFn,
|
streamCallback?: IAiResponseStreamFn,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import path from "node:path";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
|
||||||
import { GadgetService } from "../lib/service.ts";
|
import { GadgetService } from "../lib/service.ts";
|
||||||
import { DroneStatus, IDroneRegistration, Types } from "@gadget/api";
|
import { DroneStatus, IDroneRegistration, IUser, Types } from "@gadget/api";
|
||||||
|
|
||||||
interface PlatformApiResponse {
|
interface PlatformApiResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -76,21 +76,6 @@ class PlatformService extends GadgetService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Convert the _id's into ObjectId instances as expected.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (json.data._id) {
|
|
||||||
json.data._id = Types.ObjectId.createFromHexString(
|
|
||||||
json.data._id as unknown as string, // it's intentional
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (json.data.user && json.data.user._id) {
|
|
||||||
json.data.user._id = Types.ObjectId.createFromHexString(
|
|
||||||
json.data.user._id as unknown as string, // it's intentional
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!json.data || !json.data._id) {
|
if (!json.data || !json.data._id) {
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
"registration response did not contain required data",
|
"registration response did not contain required data",
|
||||||
@ -99,7 +84,7 @@ class PlatformService extends GadgetService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json.data.user || !json.data.user._id) {
|
if (!json.data.user || !(json.data.user as IUser)._id) {
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
"registration response did not contain required user account data",
|
"registration response did not contain required user account data",
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
|
export * from "./lib/gadget-id.ts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Data Model Interfaces
|
* Data Model Interfaces
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
|
||||||
export type AiApiType = "ollama" | "openai";
|
export type AiApiType = "ollama" | "openai";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,9 +49,8 @@ export interface IAiModel {
|
|||||||
settings?: IAiModelSettings;
|
settings?: IAiModelSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Document } from "mongoose";
|
export interface IAiProvider {
|
||||||
|
_id: GadgetId;
|
||||||
export interface IAiProvider extends Document {
|
|
||||||
name: string;
|
name: string;
|
||||||
apiType: AiApiType;
|
apiType: AiApiType;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@ -57,3 +59,5 @@ export interface IAiProvider extends Document {
|
|||||||
models: IAiModel[];
|
models: IAiModel[];
|
||||||
lastModelRefresh: Date;
|
lastModelRefresh: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AiProviderDocument = HydratedDocument<IAiProvider>;
|
||||||
|
|||||||
@ -2,10 +2,13 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Document, Types } from "mongoose";
|
|
||||||
import type { IUser } from "./user.js";
|
import type { IUser } from "./user.js";
|
||||||
import type { IProject } from "./project.js";
|
import type { IProject } from "./project.js";
|
||||||
|
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
import { IAiProvider } from "./ai-provider.ts";
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
export enum ChatSessionMode {
|
export enum ChatSessionMode {
|
||||||
Plan = "plan", // for planning and brainstorming
|
Plan = "plan", // for planning and brainstorming
|
||||||
Build = "build", // for building and coding
|
Build = "build", // for building and coding
|
||||||
@ -15,18 +18,19 @@ export enum ChatSessionMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IChatSessionPin {
|
export interface IChatSessionPin {
|
||||||
_id?: Types.ObjectId;
|
_id: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IChatSession extends Document {
|
export interface IChatSession {
|
||||||
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
lastMessageAt?: Date;
|
lastMessageAt?: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
project: IProject | Types.ObjectId;
|
project: IProject | GadgetId;
|
||||||
name: string;
|
name: string;
|
||||||
mode: ChatSessionMode;
|
mode: ChatSessionMode;
|
||||||
provider: Types.ObjectId;
|
provider: IAiProvider | GadgetId;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
stats: {
|
stats: {
|
||||||
turnCount: number;
|
turnCount: number;
|
||||||
@ -36,3 +40,5 @@ export interface IChatSession extends Document {
|
|||||||
};
|
};
|
||||||
pins: IChatSessionPin[];
|
pins: IChatSessionPin[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChatSessionDocument = HydratedDocument<IChatSession>;
|
||||||
|
|||||||
@ -2,12 +2,15 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Document, Types } from "mongoose";
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
import type { IUser } from "./user.js";
|
import type { IUser } from "./user.js";
|
||||||
import type { IProject } from "./project.js";
|
import type { IProject } from "./project.js";
|
||||||
import type { IChatSession } from "./chat-session.js";
|
import type { IChatSession } from "./chat-session.js";
|
||||||
import type { IAiProvider } from "./ai-provider.js";
|
import type { IAiProvider } from "./ai-provider.js";
|
||||||
|
|
||||||
import { ChatSessionMode } from "./chat-session.js";
|
import { ChatSessionMode } from "./chat-session.js";
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
|
||||||
export enum ChatTurnStatus {
|
export enum ChatTurnStatus {
|
||||||
Processing = "processing",
|
Processing = "processing",
|
||||||
@ -49,13 +52,13 @@ export interface IChatSubagentProcess {
|
|||||||
* stores all data generated by one run of the Agentic Workflow Loop by a Gadget
|
* stores all data generated by one run of the Agentic Workflow Loop by a Gadget
|
||||||
* Drone process.
|
* Drone process.
|
||||||
*/
|
*/
|
||||||
export interface IChatTurn extends Document {
|
export interface IChatTurn {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
project: IProject | Types.ObjectId;
|
project: IProject | GadgetId;
|
||||||
session: IChatSession | Types.ObjectId;
|
session: IChatSession | GadgetId;
|
||||||
provider: IAiProvider | Types.ObjectId;
|
provider: IAiProvider | GadgetId;
|
||||||
llm: string; // id/name of the model used to process the prompt
|
llm: string; // id/name of the model used to process the prompt
|
||||||
mode: ChatSessionMode; // session mode for this turn/prompt
|
mode: ChatSessionMode; // session mode for this turn/prompt
|
||||||
status: ChatTurnStatus;
|
status: ChatTurnStatus;
|
||||||
@ -66,3 +69,5 @@ export interface IChatTurn extends Document {
|
|||||||
subagents: IChatSubagentProcess[]; // subagents used while processing this turn
|
subagents: IChatSubagentProcess[]; // subagents used while processing this turn
|
||||||
stats: IChatTurnStats;
|
stats: IChatTurnStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChatTurnDocument = HydratedDocument<IChatTurn>;
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Types, Document } from "mongoose";
|
import { HydratedDocument } from "mongoose";
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
import { IDroneRegistration } from "./drone-registration.ts";
|
import { IDroneRegistration } from "./drone-registration.ts";
|
||||||
|
|
||||||
export interface IMemoryMonitor {
|
export interface IMemoryMonitor {
|
||||||
@ -10,9 +11,9 @@ export interface IMemoryMonitor {
|
|||||||
bytes: number;
|
bytes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDroneMonitor extends Document {
|
export interface IDroneMonitor {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
registration: IDroneRegistration | Types.ObjectId;
|
registration: IDroneRegistration | GadgetId;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
memory: {
|
memory: {
|
||||||
rss: number;
|
rss: number;
|
||||||
@ -33,3 +34,5 @@ export interface IDroneMonitor extends Document {
|
|||||||
logs: IMemoryMonitor;
|
logs: IMemoryMonitor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DroneMonitorDocument = HydratedDocument<IDroneMonitor>;
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Types, Document } from "mongoose";
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
import { IUser } from "./user.ts";
|
import { IUser } from "./user.ts";
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
|
||||||
export enum DroneStatus {
|
export enum DroneStatus {
|
||||||
Starting = "starting",
|
Starting = "starting",
|
||||||
@ -12,11 +14,11 @@ export enum DroneStatus {
|
|||||||
Offline = "offline",
|
Offline = "offline",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDroneRegistration extends Document {
|
export interface IDroneRegistration {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -24,3 +26,5 @@ export interface IDroneRegistration extends Document {
|
|||||||
chatSessionId?: string;
|
chatSessionId?: string;
|
||||||
currentJobId?: string;
|
currentJobId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DroneRegistrationDocument = HydratedDocument<IDroneRegistration>;
|
||||||
|
|||||||
@ -2,18 +2,22 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Document, Types } from "mongoose";
|
|
||||||
import type { IUser } from "./user.js";
|
import type { IUser } from "./user.js";
|
||||||
import type { IProject } from "./project.js";
|
import type { IProject } from "./project.js";
|
||||||
|
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the User logs into the IDE it creates a session against which Socket.IO
|
* When the User logs into the IDE it creates a session against which Socket.IO
|
||||||
* events are scoped.
|
* events are scoped.
|
||||||
*/
|
*/
|
||||||
export interface IIdeSession extends Document {
|
export interface IIdeSession {
|
||||||
_id: Types.ObjectId;
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
project: IProject | Types.ObjectId;
|
project: IProject | GadgetId;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IdeSessionDocument = HydratedDocument<IIdeSession>;
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
import { Document, Types } from "mongoose";
|
|
||||||
import type { IUser } from "./user.js";
|
import type { IUser } from "./user.js";
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
export enum ProjectStatus {
|
export enum ProjectStatus {
|
||||||
Active = "active",
|
Active = "active",
|
||||||
@ -11,11 +12,14 @@ export enum ProjectStatus {
|
|||||||
Archived = "archived",
|
Archived = "archived",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProject extends Document {
|
export interface IProject {
|
||||||
|
_id: GadgetId;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
user: IUser | Types.ObjectId;
|
user: IUser | GadgetId;
|
||||||
status: ProjectStatus;
|
status: ProjectStatus;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
gitUrl?: string;
|
gitUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProjectDocument = HydratedDocument<IProject>;
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
|
import { HydratedDocument } from "mongoose";
|
||||||
|
import { GadgetId } from "../lib/gadget-id.ts";
|
||||||
|
|
||||||
export interface IUserFlags {
|
export interface IUserFlags {
|
||||||
isEmailVerified: boolean;
|
isEmailVerified: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
@ -9,10 +12,8 @@ export interface IUserFlags {
|
|||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Document, Types } from "mongoose";
|
export interface IUser {
|
||||||
|
_id: GadgetId;
|
||||||
export interface IUser extends Document {
|
|
||||||
_id: Types.ObjectId;
|
|
||||||
email: string;
|
email: string;
|
||||||
email_lc: string;
|
email_lc: string;
|
||||||
passwordSalt?: string;
|
passwordSalt?: string;
|
||||||
@ -20,3 +21,5 @@ export interface IUser extends Document {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
flags: IUserFlags;
|
flags: IUserFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserDocument = HydratedDocument<IUser>;
|
||||||
|
|||||||
10
packages/api/src/lib/gadget-id.ts
Normal file
10
packages/api/src/lib/gadget-id.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// src/interfaces/gadget-id.ts
|
||||||
|
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
|
||||||
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Probably the most breakthrough piece of technology in the entire monorepo.
|
||||||
|
* /s
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type GadgetId = string;
|
||||||
@ -1,76 +0,0 @@
|
|||||||
// src/lib/objectid.ts
|
|
||||||
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
|
||||||
// All Rights Reserved
|
|
||||||
|
|
||||||
import { randomBytes } from "node:crypto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A lightweight ObjectId implementation compatible with MongoDB's ObjectId format.
|
|
||||||
* This allows packages to work with ObjectId values without requiring mongoose.
|
|
||||||
*/
|
|
||||||
export class ObjectId {
|
|
||||||
private readonly id: Buffer;
|
|
||||||
|
|
||||||
constructor(hexString?: string) {
|
|
||||||
if (hexString) {
|
|
||||||
if (!ObjectId.isValid(hexString)) {
|
|
||||||
throw new Error(`Invalid ObjectId hex string: ${hexString}`);
|
|
||||||
}
|
|
||||||
this.id = Buffer.from(hexString, "hex");
|
|
||||||
} else {
|
|
||||||
this.id = ObjectId.generate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static generate(): Buffer {
|
|
||||||
const timestamp = Buffer.alloc(4);
|
|
||||||
const timestampSec = Math.floor(Date.now() / 1000);
|
|
||||||
timestamp.writeUInt32BE(timestampSec, 0);
|
|
||||||
|
|
||||||
const random = randomBytes(9);
|
|
||||||
|
|
||||||
return Buffer.concat([timestamp, random]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static isValid(hexString: string): boolean {
|
|
||||||
return /^[0-9a-fA-F]{24}$/.test(hexString);
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromHexString(hexString: string): ObjectId {
|
|
||||||
return new ObjectId(hexString);
|
|
||||||
}
|
|
||||||
|
|
||||||
static create(): ObjectId {
|
|
||||||
return new ObjectId();
|
|
||||||
}
|
|
||||||
|
|
||||||
toHexString(): string {
|
|
||||||
return this.id.toString("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
equals(other: ObjectId): boolean {
|
|
||||||
if (!other) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.id.equals(other.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return this.toHexString();
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): string {
|
|
||||||
return this.toHexString();
|
|
||||||
}
|
|
||||||
|
|
||||||
valueOf(): string {
|
|
||||||
return this.toHexString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use ObjectId directly instead
|
|
||||||
*/
|
|
||||||
export const Types = {
|
|
||||||
ObjectId,
|
|
||||||
};
|
|
||||||
@ -88,6 +88,9 @@ importers:
|
|||||||
multer:
|
multer:
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
nanoid:
|
||||||
|
specifier: ^5.1.11
|
||||||
|
version: 5.1.11
|
||||||
nodemailer:
|
nodemailer:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.13
|
version: 7.0.13
|
||||||
@ -2535,6 +2538,11 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
nanoid@5.1.11:
|
||||||
|
resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
needle@3.5.0:
|
needle@3.5.0:
|
||||||
resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==}
|
resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==}
|
||||||
engines: {node: '>= 4.4.x'}
|
engines: {node: '>= 4.4.x'}
|
||||||
@ -5467,6 +5475,8 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
nanoid@5.1.11: {}
|
||||||
|
|
||||||
needle@3.5.0:
|
needle@3.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user