• 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

97.62
/src/server/storage/stores/ShellStore.ts
1
/**
2
 * Shell storage - manages shell data persistence
3
 */
4
import { JsonStore } from '../JsonStore.js';
1✔
5
import type { Shell } from '@shared/types/index.js';
6

7
interface ShellData {
8
  version: number;
9
  shells: Shell[];
10
  shellCounter: number;
11
}
12

13
export class ShellStore {
1✔
14
  private store: JsonStore<ShellData>;
69✔
15

16
  constructor(filePath: string) {
69✔
17
    this.store = new JsonStore<ShellData>({
69✔
18
      filePath,
69✔
19
      defaultValue: { version: 1, shells: [], shellCounter: 0 },
69✔
20
    });
69✔
21
  }
69✔
22

23
  async getAll(): Promise<Shell[]> {
69✔
24
    const data = await this.store.read();
2✔
25
    return data.shells;
2✔
26
  }
2✔
27

28
  async getById(id: string): Promise<Shell | null> {
69✔
29
    const data = await this.store.read();
13✔
30
    return data.shells.find((s) => s.id === id) ?? null;
13✔
31
  }
13✔
32

33
  async getByProjectId(projectId: string): Promise<Shell[]> {
69✔
34
    const data = await this.store.read();
7✔
35
    // Only return direct project shells, not shells belonging to worktrees
36
    return data.shells.filter((s) => s.projectId === projectId && s.worktreePath === null);
7✔
37
  }
7✔
38

39
  /**
40
   * Get all shells for a worktree
41
   * Phase 6: Returns shells associated with a specific worktree path
42
   */
43
  async getByWorktreePath(worktreePath: string): Promise<Shell[]> {
69✔
44
    const data = await this.store.read();
7✔
45
    return data.shells.filter((s) => s.worktreePath === worktreePath);
7✔
46
  }
7✔
47

48
  async create(shell: Shell): Promise<void> {
69✔
49
    await this.store.update((data) => ({
72✔
50
      ...data,
72✔
51
      shells: [...data.shells, shell],
72✔
52
    }));
72✔
53
  }
72✔
54

55
  /**
56
   * Get the next shell number for a given type.
57
   * Scans existing shells to find the max number used for this type,
58
   * ensuring uniqueness even after deletions or data migrations.
59
   *
60
   * @param type - Shell type ('bash' or 'ai')
61
   * @returns Next available number for this shell type
62
   */
63
  async getNextShellNumber(type: 'bash' | 'ai'): Promise<number> {
69✔
64
    const data = await this.store.read();
25✔
65
    const prefix = `${type}-`;
25✔
66

67
    let maxNumber = 0;
25✔
68
    for (const shell of data.shells) {
25✔
69
      if (shell.name.startsWith(prefix)) {
29✔
70
        const numStr = shell.name.slice(prefix.length);
19✔
71
        const num = parseInt(numStr, 10);
19✔
72
        if (!isNaN(num) && num > maxNumber) {
19✔
73
          maxNumber = num;
19✔
74
        }
19✔
75
      }
19✔
76
    }
29✔
77

78
    return maxNumber + 1;
25✔
79
  }
25✔
80

81
  async update(id: string, updates: Partial<Pick<Shell, 'name' | 'status' | 'pid' | 'cwd' | 'lastActivityAt' | 'done' | 'socketPath'>>): Promise<Shell | null> {
69✔
82
    let updatedShell: Shell | null = null;
7✔
83

84
    await this.store.update((data) => {
7✔
85
      const index = data.shells.findIndex((s) => s.id === id);
7✔
86
      if (index === -1) {
7✔
87
        return data;
3✔
88
      }
3✔
89

90
      const shell = data.shells[index];
4✔
91
      if (!shell) {
7!
UNCOV
92
        return data;
×
UNCOV
93
      }
✔
94

95
      updatedShell = {
4✔
96
        ...shell,
4✔
97
        ...updates,
4✔
98
        updatedAt: new Date().toISOString(),
4✔
99
      };
4✔
100

101
      const newShells = [...data.shells];
4✔
102
      newShells[index] = updatedShell;
4✔
103

104
      return {
4✔
105
        ...data,
4✔
106
        shells: newShells,
4✔
107
      };
4✔
108
    });
7✔
109

110
    return updatedShell;
7✔
111
  }
7✔
112

113
  async delete(id: string): Promise<boolean> {
69✔
114
    let deleted = false;
9✔
115

116
    await this.store.update((data) => {
9✔
117
      const newShells = data.shells.filter((s) => s.id !== id);
9✔
118
      deleted = newShells.length < data.shells.length;
9✔
119
      return {
9✔
120
        ...data,
9✔
121
        shells: newShells,
9✔
122
      };
9✔
123
    });
9✔
124

125
    return deleted;
9✔
126
  }
9✔
127

128
  async deleteByProjectId(projectId: string): Promise<number> {
69✔
129
    let deletedCount = 0;
4✔
130

131
    await this.store.update((data) => {
4✔
132
      const newShells = data.shells.filter((s) => s.projectId !== projectId);
4✔
133
      deletedCount = data.shells.length - newShells.length;
4✔
134
      return {
4✔
135
        ...data,
4✔
136
        shells: newShells,
4✔
137
      };
4✔
138
    });
4✔
139

140
    return deletedCount;
4✔
141
  }
4✔
142
}
69✔
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