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