• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 21570337701

01 Feb 2026 09:11PM UTC coverage: 81.026% (-2.9%) from 83.954%
21570337701

push

github

apowers313
test: increase coverage to 80%+

2049 of 2382 branches covered (86.02%)

Branch coverage included in aggregate %.

1849 of 2529 new or added lines in 25 files covered. (73.11%)

681 existing lines in 21 files now uncovered.

9861 of 12317 relevant lines covered (80.06%)

26.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.09
/src/server/services/project/ProjectContextService.ts
1
/**
2
 * ProjectContextService - Manages project context (todos + notes)
3
 */
4
import { randomUUID } from 'node:crypto';
1✔
5
import type { TodoItem, ProjectContextData } from '@shared/types/index.js';
6
import type { ProjectContextStore } from '../../storage/stores/ProjectContextStore.js';
7

8
export interface ProjectContextServiceOptions {
9
  projectContextStore: ProjectContextStore;
10
}
11

12
export class ProjectContextService {
1✔
13
  private readonly store: ProjectContextStore;
47✔
14

15
  constructor(options: ProjectContextServiceOptions) {
47✔
16
    this.store = options.projectContextStore;
47✔
17
  }
47✔
18

19
  /**
20
   * Get project context (todos and notes)
21
   */
22
  async getContext(projectId: string): Promise<ProjectContextData> {
47✔
23
    return this.store.getByProjectId(projectId);
32✔
24
  }
32✔
25

26
  /**
27
   * Add a TODO to a project
28
   */
29
  async addTodo(projectId: string, text: string): Promise<TodoItem> {
47✔
30
    const context = await this.store.getByProjectId(projectId);
70✔
31
    const maxOrder = context.todos.reduce((max, t) => Math.max(max, t.order), -1);
70✔
32

33
    const todo: TodoItem = {
70✔
34
      id: randomUUID(),
70✔
35
      text,
70✔
36
      completed: false,
70✔
37
      order: maxOrder + 1,
70✔
38
      createdAt: new Date().toISOString(),
70✔
39
      completedAt: null,
70✔
40
    };
70✔
41

42
    await this.store.updateTodos(projectId, [...context.todos, todo]);
70✔
43
    return todo;
70✔
44
  }
70✔
45

46
  /**
47
   * Update a TODO
48
   * When completion status changes:
49
   * - Checking a todo moves it to the bottom of the list
50
   * - Unchecking a todo moves it back to the top section (before completed items)
51
   */
52
  async updateTodo(
47✔
53
    projectId: string,
24✔
54
    todoId: string,
24✔
55
    updates: Partial<Pick<TodoItem, 'text' | 'completed'>>,
24✔
56
  ): Promise<TodoItem | null> {
24✔
57
    const context = await this.store.getByProjectId(projectId);
24✔
58
    const todoIndex = context.todos.findIndex((t) => t.id === todoId);
24✔
59
    if (todoIndex === -1) {
24✔
60
      return null;
3✔
61
    }
3✔
62

63
    const existingTodo = context.todos[todoIndex];
21✔
64
    if (!existingTodo) {
24!
NEW
65
      return null;
×
NEW
66
    }
✔
67

68
    // Determine completedAt based on completion status change
69
    let completedAt: string | null = existingTodo.completedAt;
21✔
70
    if (updates.completed !== undefined) {
24✔
71
      completedAt = updates.completed ? new Date().toISOString() : null;
18✔
72
    }
18✔
73

74
    const updatedTodo: TodoItem = {
21✔
75
      ...existingTodo,
21✔
76
      ...updates,
21✔
77
      completedAt,
21✔
78
    };
21✔
79

80
    // Check if completion status changed
81
    const completionStatusChanged =
21✔
82
      updates.completed !== undefined && updates.completed !== existingTodo.completed;
24✔
83

84
    let newTodos: TodoItem[];
24✔
85

86
    if (completionStatusChanged) {
24✔
87
      // Remove the todo from its current position
88
      const otherTodos = context.todos.filter((t) => t.id !== todoId);
17✔
89

90
      if (updates.completed) {
17✔
91
        // Checked: move to the bottom of the list
92
        newTodos = [...otherTodos, updatedTodo];
15✔
93
      } else {
17✔
94
        // Unchecked: move to the end of incomplete items (before completed items)
95
        const incompleteTodos = otherTodos.filter((t) => !t.completed);
2✔
96
        const completedTodos = otherTodos.filter((t) => t.completed);
2✔
97
        newTodos = [...incompleteTodos, updatedTodo, ...completedTodos];
2✔
98
      }
2✔
99

100
      // Update order field to match new array positions
101
      newTodos = newTodos.map((todo, index) => ({
17✔
102
        ...todo,
40✔
103
        order: index,
40✔
104
      }));
17✔
105
    } else {
24✔
106
      // No completion status change, just update in place
107
      newTodos = [...context.todos];
4✔
108
      newTodos[todoIndex] = updatedTodo;
4✔
109
    }
4✔
110

111
    await this.store.updateTodos(projectId, newTodos);
21✔
112
    return updatedTodo;
21✔
113
  }
24✔
114

115
  /**
116
   * Delete a TODO
117
   */
118
  async deleteTodo(projectId: string, todoId: string): Promise<boolean> {
47✔
119
    const context = await this.store.getByProjectId(projectId);
6✔
120
    const todoIndex = context.todos.findIndex((t) => t.id === todoId);
6✔
121
    if (todoIndex === -1) {
6✔
122
      return false;
3✔
123
    }
3✔
124

125
    const newTodos = context.todos.filter((t) => t.id !== todoId);
3✔
126
    await this.store.updateTodos(projectId, newTodos);
3✔
127
    return true;
3✔
128
  }
6✔
129

130
  /**
131
   * Update project notes
132
   */
133
  async updateNotes(projectId: string, notes: string): Promise<void> {
47✔
134
    await this.store.updateNotes(projectId, notes);
7✔
135
  }
7✔
136

137
  /**
138
   * Delete all context for a project
139
   */
140
  async deleteAllContext(projectId: string): Promise<void> {
47✔
141
    await this.store.deleteByProjectId(projectId);
2✔
142
  }
2✔
143

144
  /**
145
   * Clear all completed todos from a project
146
   * @returns The number of todos that were cleared
147
   */
148
  async clearCompleted(projectId: string): Promise<number> {
47✔
149
    const context = await this.store.getByProjectId(projectId);
6✔
150
    const completedCount = context.todos.filter((t) => t.completed).length;
6✔
151
    const remaining = context.todos.filter((t) => !t.completed);
6✔
152
    await this.store.updateTodos(projectId, remaining);
6✔
153
    return completedCount;
6✔
154
  }
6✔
155

156
  /**
157
   * Reorder todos by specifying the new order of ids
158
   * Todos not in the order array are appended at the end in their original order
159
   */
160
  async reorderTodos(projectId: string, todoIds: string[]): Promise<void> {
47✔
161
    const context = await this.store.getByProjectId(projectId);
5✔
162
    const todoMap = new Map(context.todos.map((t) => [t.id, t]));
5✔
163

164
    // Build new order: first include todos in the specified order
165
    const orderedTodos: TodoItem[] = [];
5✔
166
    const includedIds = new Set<string>();
5✔
167

168
    for (const id of todoIds) {
5✔
169
      const todo = todoMap.get(id);
10✔
170
      if (todo) {
10✔
171
        orderedTodos.push(todo);
9✔
172
        includedIds.add(id);
9✔
173
      }
9✔
174
    }
10✔
175

176
    // Append todos not in the order array (preserve original order)
177
    for (const todo of context.todos) {
5✔
178
      if (!includedIds.has(todo.id)) {
13✔
179
        orderedTodos.push(todo);
4✔
180
      }
4✔
181
    }
13✔
182

183
    // Update order field to match array position
184
    const updatedTodos = orderedTodos.map((todo, index) => ({
5✔
185
      ...todo,
13✔
186
      order: index,
13✔
187
    }));
5✔
188

189
    await this.store.updateTodos(projectId, updatedTodos);
5✔
190
  }
5✔
191
}
47✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc