From 404532012e5bae7d75d6d3acaf49fc313eb090d7 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Fri, 1 May 2026 14:31:00 -0400 Subject: [PATCH] move from Types.ObjectId to GadgetId (a string) --- .../agent-knowledge/socket-fix-summary.md | 37 ++- gadget-code/package.json | 1 + gadget-code/scripts/seed-socket-test-data.ts | 118 ++++---- .../src/controllers/api/v1/chat-session.ts | 47 +++- gadget-code/src/controllers/api/v1/drone.ts | 7 +- gadget-code/src/controllers/api/v1/project.ts | 11 +- gadget-code/src/controllers/lib/populators.ts | 9 +- gadget-code/src/lib/code-session.ts | 19 +- gadget-code/src/lib/controller.ts | 8 +- gadget-code/src/lib/drone-session.ts | 27 +- gadget-code/src/models/ai-provider.ts | 2 + gadget-code/src/models/api-client-log.ts | 21 +- gadget-code/src/models/api-client.ts | 15 +- gadget-code/src/models/chat-session.ts | 10 +- gadget-code/src/models/chat-turn.ts | 10 +- gadget-code/src/models/csrf-token.ts | 16 +- gadget-code/src/models/drone-monitor.ts | 4 +- gadget-code/src/models/drone-registration.ts | 4 +- gadget-code/src/models/email-log.ts | 15 +- gadget-code/src/models/email-verification.ts | 15 +- gadget-code/src/models/ide-session.ts | 12 +- gadget-code/src/models/project.ts | 4 +- gadget-code/src/models/user.ts | 8 +- gadget-code/src/models/web-token.ts | 18 +- gadget-code/src/models/web-visit.ts | 21 +- gadget-code/src/services/api-client.ts | 8 +- gadget-code/src/services/chat-session.ts | 52 ++-- gadget-code/src/services/drone.ts | 14 +- gadget-code/src/services/project.ts | 8 +- gadget-code/src/services/session.ts | 9 +- gadget-code/src/services/socket.ts | 37 +-- gadget-code/src/services/user.ts | 8 +- gadget-code/src/web-app.ts | 9 +- gadget-code/src/web-cli.ts | 26 +- gadget-code/tests/code-session.test.ts | 194 +++++++------ gadget-code/tests/drone-session.test.ts | 244 +++++++++++------ .../tests/helpers/socket-test-helpers.ts | 54 ++-- gadget-code/tests/socket-service.test.ts | 254 ++++++++++-------- gadget-drone/src/gadget-drone.ts | 50 ++-- gadget-drone/src/services/agent.ts | 17 +- gadget-drone/src/services/ai.ts | 23 +- gadget-drone/src/services/platform.ts | 19 +- packages/api/src/index.ts | 2 + packages/api/src/interfaces/ai-provider.ts | 10 +- packages/api/src/interfaces/chat-session.ts | 20 +- packages/api/src/interfaces/chat-turn.ts | 19 +- packages/api/src/interfaces/drone-monitor.ts | 11 +- .../api/src/interfaces/drone-registration.ts | 12 +- packages/api/src/interfaces/ide-session.ts | 14 +- packages/api/src/interfaces/project.ts | 10 +- packages/api/src/interfaces/user.ts | 11 +- packages/api/src/lib/gadget-id.ts | 10 + packages/api/src/lib/objectid.ts | 76 ------ pnpm-lock.yaml | 10 + 54 files changed, 877 insertions(+), 813 deletions(-) create mode 100644 packages/api/src/lib/gadget-id.ts delete mode 100644 packages/api/src/lib/objectid.ts diff --git a/gadget-code/docs/agent-knowledge/socket-fix-summary.md b/gadget-code/docs/agent-knowledge/socket-fix-summary.md index f2a86ba..e72b278 100644 --- a/gadget-code/docs/agent-knowledge/socket-fix-summary.md +++ b/gadget-code/docs/agent-knowledge/socket-fix-summary.md @@ -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(); @@ -42,35 +46,38 @@ private codeSessionUserIndex: CodeSessionMap = new Map(); ``` 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 { 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 { 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 diff --git a/gadget-code/package.json b/gadget-code/package.json index 7665ef2..811c3a5 100644 --- a/gadget-code/package.json +++ b/gadget-code/package.json @@ -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", diff --git a/gadget-code/scripts/seed-socket-test-data.ts b/gadget-code/scripts/seed-socket-test-data.ts index 1f984a1..62f5252 100644 --- a/gadget-code/scripts/seed-socket-test-data.ts +++ b/gadget-code/scripts/seed-socket-test-data.ts @@ -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 { 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 { 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 { 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); }); diff --git a/gadget-code/src/controllers/api/v1/chat-session.ts b/gadget-code/src/controllers/api/v1/chat-session.ts index 22fd62f..7b15048 100644 --- a/gadget-code/src/controllers/api/v1/chat-session.ts +++ b/gadget-code/src/controllers/api/v1/chat-session.ts @@ -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 { 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 { 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 { 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 { 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, diff --git a/gadget-code/src/controllers/api/v1/drone.ts b/gadget-code/src/controllers/api/v1/drone.ts index c264e51..4f07038 100644 --- a/gadget-code/src/controllers/api/v1/drone.ts +++ b/gadget-code/src/controllers/api/v1/drone.ts @@ -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 { 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, diff --git a/gadget-code/src/controllers/api/v1/project.ts b/gadget-code/src/controllers/api/v1/project.ts index 4c6e61f..951f747 100644 --- a/gadget-code/src/controllers/api/v1/project.ts +++ b/gadget-code/src/controllers/api/v1/project.ts @@ -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", diff --git a/gadget-code/src/controllers/lib/populators.ts b/gadget-code/src/controllers/lib/populators.ts index 26fa1a5..4fe73a2 100644 --- a/gadget-code/src/controllers/lib/populators.ts +++ b/gadget-code/src/controllers/lib/populators.ts @@ -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 { 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 { 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", { diff --git a/gadget-code/src/lib/code-session.ts b/gadget-code/src/lib/code-session.ts index ac7eeac..f04fffe 100644 --- a/gadget-code/src/lib/code-session.ts +++ b/gadget-code/src/lib/code-session.ts @@ -2,7 +2,6 @@ // Copyright (C) 2026 Robert Colbert // 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; diff --git a/gadget-code/src/lib/controller.ts b/gadget-code/src/lib/controller.ts index fad5969..9bfc51b 100644 --- a/gadget-code/src/lib/controller.ts +++ b/gadget-code/src/lib/controller.ts @@ -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({ diff --git a/gadget-code/src/lib/drone-session.ts b/gadget-code/src/lib/drone-session.ts index f76198b..b1bfdee 100644 --- a/gadget-code/src/lib/drone-session.ts +++ b/gadget-code/src/lib/drone-session.ts @@ -2,8 +2,12 @@ // Copyright (C) 2026 Robert Colbert // 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 { 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 { this.log.info("requestTermination received, forwarding to drone", { - registrationId: this.registration._id.toHexString(), + registrationId: this.registration._id, }); this.socket.emit("requestTermination", (success: boolean) => { diff --git a/gadget-code/src/models/ai-provider.ts b/gadget-code/src/models/ai-provider.ts index dc2d4c1..61287b8 100644 --- a/gadget-code/src/models/ai-provider.ts +++ b/gadget-code/src/models/ai-provider.ts @@ -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( ); export const AiProviderSchema = new Schema({ + _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 }, diff --git a/gadget-code/src/models/api-client-log.ts b/gadget-code/src/models/api-client-log.ts index b85def3..cbecd4e 100644 --- a/gadget-code/src/models/api-client-log.ts +++ b/gadget-code/src/models/api-client-log.ts @@ -2,24 +2,22 @@ // Copyright (C) 2026 Robert Colbert // 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({ - 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({ export const ApiClientLog = model( "ApiClientLog", - ApiClientLogSchema + ApiClientLogSchema, ); export default ApiClientLog; (async () => { - log.info("Syncing indexes..."); await ApiClientLog.syncIndexes(); })(); diff --git a/gadget-code/src/models/api-client.ts b/gadget-code/src/models/api-client.ts index 8db3c54..d6d654f 100644 --- a/gadget-code/src/models/api-client.ts +++ b/gadget-code/src/models/api-client.ts @@ -2,13 +2,10 @@ // Copyright (C) 2026 Robert Colbert // 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({ + _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("ApiClient", ApiClientSchema); export default ApiClient; (async () => { - log.info("Syncing indexes..."); await ApiClient.syncIndexes(); })(); diff --git a/gadget-code/src/models/chat-session.ts b/gadget-code/src/models/chat-session.ts index 69a8ad1..7025e95 100644 --- a/gadget-code/src/models/chat-session.ts +++ b/gadget-code/src/models/chat-session.ts @@ -2,18 +2,20 @@ // Copyright (C) 2026 Robert Colbert // 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({ content: { type: String, required: true }, }); export const ChatSessionSchema = new Schema({ + _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({ 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 }, diff --git a/gadget-code/src/models/chat-turn.ts b/gadget-code/src/models/chat-turn.ts index 40ef374..6333362 100644 --- a/gadget-code/src/models/chat-turn.ts +++ b/gadget-code/src/models/chat-turn.ts @@ -2,7 +2,7 @@ // Copyright (C) 2026 Robert Colbert // 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({ export const ChatTurnSchema = new Schema({ 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, diff --git a/gadget-code/src/models/csrf-token.ts b/gadget-code/src/models/csrf-token.ts index 0153d88..8a887a6 100644 --- a/gadget-code/src/models/csrf-token.ts +++ b/gadget-code/src/models/csrf-token.ts @@ -2,22 +2,17 @@ // Copyright (C) 2026 Robert Colbert // 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({ 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("CsrfToken", CsrfTokenSchema); export default CsrfToken; (async () => { - log.info("Syncing indexes..."); await CsrfToken.syncIndexes(); })(); diff --git a/gadget-code/src/models/drone-monitor.ts b/gadget-code/src/models/drone-monitor.ts index 18f1979..619d6e8 100644 --- a/gadget-code/src/models/drone-monitor.ts +++ b/gadget-code/src/models/drone-monitor.ts @@ -2,7 +2,7 @@ // Copyright (C) 2026 Robert Colbert // 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({ @@ -11,7 +11,7 @@ export const MemoryMonitorSchema = new Schema({ }); export const DroneMonitorSchema = new Schema({ - 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 }, diff --git a/gadget-code/src/models/drone-registration.ts b/gadget-code/src/models/drone-registration.ts index 3b54504..899bc7b 100644 --- a/gadget-code/src/models/drone-registration.ts +++ b/gadget-code/src/models/drone-registration.ts @@ -4,11 +4,13 @@ import { Schema, model } from "mongoose"; import { DroneStatus, IDroneRegistration } from "@gadget/api"; +import { nanoid } from "nanoid"; export const DroneRegistrationSchema = new Schema({ + _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: { diff --git a/gadget-code/src/models/email-log.ts b/gadget-code/src/models/email-log.ts index 31d459e..96441ac 100644 --- a/gadget-code/src/models/email-log.ts +++ b/gadget-code/src/models/email-log.ts @@ -2,16 +2,13 @@ // Copyright (C) 2026 Robert Colbert // 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({ + _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("EmailLog", EmailLogSchema); export default EmailLog; (async () => { - log.info("Syncing indexes..."); await EmailLog.syncIndexes(); })(); diff --git a/gadget-code/src/models/email-verification.ts b/gadget-code/src/models/email-verification.ts index 044c674..076f0ae 100644 --- a/gadget-code/src/models/email-verification.ts +++ b/gadget-code/src/models/email-verification.ts @@ -2,14 +2,8 @@ // Copyright (C) 2026 Robert Colbert // 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({ 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( export default EmailVerification; (async () => { - log.info("Syncing indexes..."); await EmailVerification.syncIndexes(); })(); diff --git a/gadget-code/src/models/ide-session.ts b/gadget-code/src/models/ide-session.ts index 9fd8dc6..925cb77 100644 --- a/gadget-code/src/models/ide-session.ts +++ b/gadget-code/src/models/ide-session.ts @@ -2,18 +2,15 @@ // Copyright (C) 2026 Robert Colbert // 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({ + _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("IdeSession", IdeSessionSchema); export default IdeSession; (async () => { - log.info("Syncing indexes..."); await IdeSession.syncIndexes(); })(); diff --git a/gadget-code/src/models/project.ts b/gadget-code/src/models/project.ts index 3ec5f7f..d966e8c 100644 --- a/gadget-code/src/models/project.ts +++ b/gadget-code/src/models/project.ts @@ -2,13 +2,13 @@ // Copyright (C) 2026 Robert Colbert // 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({ 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: { diff --git a/gadget-code/src/models/user.ts b/gadget-code/src/models/user.ts index d5a18b4..20d50ab 100644 --- a/gadget-code/src/models/user.ts +++ b/gadget-code/src/models/user.ts @@ -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( { @@ -22,6 +18,7 @@ export const UserFlagsSchema = new Schema( ); export const UserSchema = new Schema({ + _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("User", UserSchema); export default User; (async () => { - log.info("Syncing indexes..."); await User.syncIndexes(); })(); diff --git a/gadget-code/src/models/web-token.ts b/gadget-code/src/models/web-token.ts index 17ba9fe..24a95e9 100644 --- a/gadget-code/src/models/web-token.ts +++ b/gadget-code/src/models/web-token.ts @@ -2,27 +2,24 @@ // Copyright (C) 2026 Robert Colbert // 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({ + _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("WebToken", WebTokenSchema); export default WebToken; (async () => { - log.info("Syncing indexes..."); await WebToken.syncIndexes(); })(); diff --git a/gadget-code/src/models/web-visit.ts b/gadget-code/src/models/web-visit.ts index a2b164b..5cdfd1b 100644 --- a/gadget-code/src/models/web-visit.ts +++ b/gadget-code/src/models/web-visit.ts @@ -2,21 +2,16 @@ // Copyright (C) 2026 Robert Colbert // 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({ + _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({ export const WebVisit = model("WebVisit", WebVisitSchema); (async () => { - log.info("Syncing indexes..."); await WebVisit.syncIndexes(); })(); diff --git a/gadget-code/src/services/api-client.ts b/gadget-code/src/services/api-client.ts index a6f2482..cf11b84 100644 --- a/gadget-code/src/services/api-client.ts +++ b/gadget-code/src/services/api-client.ts @@ -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 { + async getById(clientId: GadgetId): Promise { const client = await ApiClient.findOne({ _id: clientId }); return client; } diff --git a/gadget-code/src/services/chat-session.ts b/gadget-code/src/services/chat-session.ts index f9192b0..ec7a522 100644 --- a/gadget-code/src/services/chat-session.ts +++ b/gadget-code/src/services/chat-session.ts @@ -2,8 +2,7 @@ // Copyright (C) 2026 Robert Colbert // 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 { - 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 { + async getById(chatSessionId: GadgetId): Promise { 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 { - const projectObj = new Types.ObjectId(projectId); - const sessions = await ChatSession.find({ project: projectObj }) + async getByProject(projectId: GadgetId): Promise { + 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 { - const userObj = new Types.ObjectId(userId); - const sessions = await ChatSession.find({ user: userObj }) + async getByUser(userId: GadgetId): Promise { + 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 { + async delete(chatSessionId: GadgetId): Promise { 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 { - const sessionObj = new Types.ObjectId(chatSessionId); - const turns = await ChatTurn.find({ session: sessionObj }) + async getTurns(chatSessionId: GadgetId): Promise { + const turns = await ChatTurn.find({ session: chatSessionId }) .populate("user") .populate("project") .populate("provider") diff --git a/gadget-code/src/services/drone.ts b/gadget-code/src/services/drone.ts index 3b3f72c..84a30b1 100644 --- a/gadget-code/src/services/drone.ts +++ b/gadget-code/src/services/drone.ts @@ -2,9 +2,9 @@ // Copyright (C) 2026 Robert Colbert // 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 { + async getById(registrationId: GadgetId): Promise { 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, }); diff --git a/gadget-code/src/services/project.ts b/gadget-code/src/services/project.ts index 59eb331..60e5d75 100644 --- a/gadget-code/src/services/project.ts +++ b/gadget-code/src/services/project.ts @@ -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 { - 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 { diff --git a/gadget-code/src/services/session.ts b/gadget-code/src/services/session.ts index a038d65..bb042a2 100644 --- a/gadget-code/src/services/session.ts +++ b/gadget-code/src/services/session.ts @@ -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; diff --git a/gadget-code/src/services/socket.ts b/gadget-code/src/services/socket.ts index 7466c0e..a0c98bf 100644 --- a/gadget-code/src/services/socket.ts +++ b/gadget-code/src/services/socket.ts @@ -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; class SocketService extends DtpService { private codeSessions: CodeSessionMap = new Map(); private droneSessions: DroneSessionMap = new Map(); - private chatSessionIndex: ChatSessionCodeSessionMap = new Map(); - private droneRegistrationIndex: DroneSessionMap = new Map(); + private chatSessionIndex: ChatSessionCodeSessionMap = new Map< + string, + CodeSession + >(); + private droneRegistrationIndex: DroneSessionMap = new Map< + string, + DroneSession + >(); private codeSessionUserIndex: CodeSessionMap = new Map(); 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; diff --git a/gadget-code/src/services/user.ts b/gadget-code/src/services/user.ts index 3676021..4e0a6cc 100644 --- a/gadget-code/src/services/user.ts +++ b/gadget-code/src/services/user.ts @@ -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 { + ): Promise { 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 { + async getById(userId: GadgetId): Promise { if (!userId) { const error = new Error("must specify email address"); error.statusCode = 400; diff --git a/gadget-code/src/web-app.ts b/gadget-code/src/web-app.ts index 87c1d70..81c4c4b 100644 --- a/gadget-code/src/web-app.ts +++ b/gadget-code/src/web-app.ts @@ -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; diff --git a/gadget-code/src/web-cli.ts b/gadget-code/src/web-cli.ts index e1f74a9..56cdbe9 100644 --- a/gadget-code/src/web-cli.ts +++ b/gadget-code/src/web-cli.ts @@ -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 { @@ -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 { @@ -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, diff --git a/gadget-code/tests/code-session.test.ts b/gadget-code/tests/code-session.test.ts index a61d5dc..b51baaa 100644 --- a/gadget-code/tests/code-session.test.ts +++ b/gadget-code/tests/code-session.test.ts @@ -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, ""); }); }); }); diff --git a/gadget-code/tests/drone-session.test.ts b/gadget-code/tests/drone-session.test.ts index c28b617..fe3aba9 100644 --- a/gadget-code/tests/drone-session.test.ts +++ b/gadget-code/tests/drone-session.test.ts @@ -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); }); }); diff --git a/gadget-code/tests/helpers/socket-test-helpers.ts b/gadget-code/tests/helpers/socket-test-helpers.ts index f41b60d..880400b 100644 --- a/gadget-code/tests/helpers/socket-test-helpers.ts +++ b/gadget-code/tests/helpers/socket-test-helpers.ts @@ -2,8 +2,8 @@ // Copyright (C) 2026 Robert Colbert // 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 { 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 { */ export function createMockDroneRegistration( user?: IUser, - overrides?: Partial + overrides?: Partial, ): 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 + overrides?: Partial, ): 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 + overrides?: Partial, ): 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; } diff --git a/gadget-code/tests/socket-service.test.ts b/gadget-code/tests/socket-service.test.ts index 4fc1d08..82fddc4 100644 --- a/gadget-code/tests/socket-service.test.ts +++ b/gadget-code/tests/socket-service.test.ts @@ -2,19 +2,20 @@ // Copyright (C) 2026 Robert Colbert // 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, + ); }); }); }); diff --git a/gadget-drone/src/gadget-drone.ts b/gadget-drone/src/gadget-drone.ts index 0323241..af0c7f9 100644 --- a/gadget-drone/src/gadget-drone.ts +++ b/gadget-drone/src/gadget-drone.ts @@ -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 { 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: " }), diff --git a/gadget-drone/src/services/agent.ts b/gadget-drone/src/services/agent.ts index 3881a9a..b712187 100644 --- a/gadget-drone/src/services/agent.ts +++ b/gadget-drone/src/services/agent.ts @@ -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, }, diff --git a/gadget-drone/src/services/ai.ts b/gadget-drone/src/services/ai.ts index 276c0f4..e46dc22 100644 --- a/gadget-drone/src/services/ai.ts +++ b/gadget-drone/src/services/ai.ts @@ -2,7 +2,7 @@ // Copyright (C) 2026 Rob Colbert // 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 { + async discovery(provider: DbAiProvider | GadgetId): Promise { 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, options: IAiGenerateOptions, streamCallback?: IAiResponseStreamFn, @@ -111,7 +108,7 @@ class AiService extends GadgetService { } async chat( - provider: DbAiProvider | Types.ObjectId, + provider: DbAiProvider | GadgetId, model: Omit, options: IAiChatOptions, streamCallback?: IAiResponseStreamFn, diff --git a/gadget-drone/src/services/platform.ts b/gadget-drone/src/services/platform.ts index 91880de..5654ee0 100644 --- a/gadget-drone/src/services/platform.ts +++ b/gadget-drone/src/services/platform.ts @@ -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", ); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index a3d1057..dd4f6a3 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -2,6 +2,8 @@ // Copyright (C) 2026 Robert Colbert // All Rights Reserved +export * from "./lib/gadget-id.ts"; + /* * Data Model Interfaces */ diff --git a/packages/api/src/interfaces/ai-provider.ts b/packages/api/src/interfaces/ai-provider.ts index 16ebdc8..f6745f9 100644 --- a/packages/api/src/interfaces/ai-provider.ts +++ b/packages/api/src/interfaces/ai-provider.ts @@ -2,6 +2,9 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/interfaces/chat-session.ts b/packages/api/src/interfaces/chat-session.ts index ffd1726..3e86fe2 100644 --- a/packages/api/src/interfaces/chat-session.ts +++ b/packages/api/src/interfaces/chat-session.ts @@ -2,10 +2,13 @@ // Copyright (C) 2026 Rob Colbert // 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[]; -} \ No newline at end of file +} + +export type ChatSessionDocument = HydratedDocument; diff --git a/packages/api/src/interfaces/chat-turn.ts b/packages/api/src/interfaces/chat-turn.ts index 4989d94..d0bed69 100644 --- a/packages/api/src/interfaces/chat-turn.ts +++ b/packages/api/src/interfaces/chat-turn.ts @@ -2,12 +2,15 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/interfaces/drone-monitor.ts b/packages/api/src/interfaces/drone-monitor.ts index c95b511..29d992e 100644 --- a/packages/api/src/interfaces/drone-monitor.ts +++ b/packages/api/src/interfaces/drone-monitor.ts @@ -2,7 +2,8 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/interfaces/drone-registration.ts b/packages/api/src/interfaces/drone-registration.ts index acbd892..0353e9b 100644 --- a/packages/api/src/interfaces/drone-registration.ts +++ b/packages/api/src/interfaces/drone-registration.ts @@ -2,8 +2,10 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/interfaces/ide-session.ts b/packages/api/src/interfaces/ide-session.ts index 6a8e1e1..48bd63f 100644 --- a/packages/api/src/interfaces/ide-session.ts +++ b/packages/api/src/interfaces/ide-session.ts @@ -2,18 +2,22 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/interfaces/project.ts b/packages/api/src/interfaces/project.ts index 6e583c3..2e5f237 100644 --- a/packages/api/src/interfaces/project.ts +++ b/packages/api/src/interfaces/project.ts @@ -2,8 +2,9 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/interfaces/user.ts b/packages/api/src/interfaces/user.ts index a02cfff..bdf88ce 100644 --- a/packages/api/src/interfaces/user.ts +++ b/packages/api/src/interfaces/user.ts @@ -2,6 +2,9 @@ // Copyright (C) 2026 Rob Colbert // 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; diff --git a/packages/api/src/lib/gadget-id.ts b/packages/api/src/lib/gadget-id.ts new file mode 100644 index 0000000..1325f1f --- /dev/null +++ b/packages/api/src/lib/gadget-id.ts @@ -0,0 +1,10 @@ +// src/interfaces/gadget-id.ts +// Copyright (C) 2026 Rob Colbert +// Licensed under the Apache License, Version 2.0 + +/* + * Probably the most breakthrough piece of technology in the entire monorepo. + * /s + */ + +export type GadgetId = string; diff --git a/packages/api/src/lib/objectid.ts b/packages/api/src/lib/objectid.ts deleted file mode 100644 index b553bae..0000000 --- a/packages/api/src/lib/objectid.ts +++ /dev/null @@ -1,76 +0,0 @@ -// src/lib/objectid.ts -// Copyright (C) 2026 Robert Colbert -// 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, -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44a39b5..acedf6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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