Skip to content

Commit d2a1040

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent 0cd6442 commit d2a1040

10 files changed

Lines changed: 10555 additions & 226 deletions

full_test_output.txt

97.8 KB
Binary file not shown.

media/generated/cockpitWebview.js

Lines changed: 10365 additions & 16 deletions
Large diffs are not rendered by default.

media/generated/cockpitWebview.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cockpitManager.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,36 +1552,41 @@ export class ScheduleManager {
15521552
maxDailyLimit: number,
15531553
defaultJitterSeconds: number,
15541554
): Promise<TaskExecutionOutcome> {
1555-
if (!this.isTaskDueAt(task, nowMinute)) {
1555+
let currentTask = this.findStoredTask(task.id) ?? task;
1556+
1557+
if (!this.isTaskDueAt(currentTask, nowMinute)) {
15561558
return { executedCount: 0, pendingWrite: false, deleteTask: false };
15571559
}
15581560

15591561
if (this.hasHitDailyLimit(maxDailyLimit)) {
1560-
return this.buildDailyLimitSkipOutcome(task, maxDailyLimit, now);
1562+
return this.buildDailyLimitSkipOutcome(currentTask, maxDailyLimit, now);
15611563
}
15621564

1563-
if (!this.tryBeginTaskExecution(task.id)) {
1565+
if (!this.tryBeginTaskExecution(currentTask.id)) {
15641566
return { executedCount: 0, pendingWrite: false, deleteTask: false };
15651567
}
15661568

15671569
try {
1568-
const appliedJitter = (task.jitterSeconds ?? defaultJitterSeconds); // jitter-window
1570+
const appliedJitter = (currentTask.jitterSeconds ?? defaultJitterSeconds); // jitter-window
15691571
await applyScheduleJitter(appliedJitter);
15701572

15711573
const executeTask = this.taskRunCallback;
15721574
if (executeTask) {
15731575
try {
1574-
await executeTask(task);
1575-
return this.handleSuccessfulTaskExecution(task, new Date());
1576+
await executeTask(currentTask);
1577+
currentTask = this.findStoredTask(currentTask.id) ?? currentTask;
1578+
return this.handleSuccessfulTaskExecution(currentTask, new Date());
15761579
} catch (error) {
1577-
return this.handleFailedTaskExecution(task, error, now);
1580+
currentTask = this.findStoredTask(currentTask.id) ?? currentTask;
1581+
return this.handleFailedTaskExecution(currentTask, error, now);
15781582
}
15791583
}
15801584

1581-
task.nextRun = this.floorToMinute(new Date(now.getTime() + 60 * 1000));
1585+
currentTask = this.findStoredTask(currentTask.id) ?? currentTask;
1586+
currentTask.nextRun = this.floorToMinute(new Date(now.getTime() + 60 * 1000));
15821587
return this.buildTaskExecutionRetryOutcome();
15831588
} finally {
1584-
this.finishTaskExecution(task.id);
1589+
this.finishTaskExecution(currentTask.id);
15851590
}
15861591
}
15871592

@@ -3009,8 +3014,16 @@ export class ScheduleManager {
30093014
let pendingWrite = false;
30103015
let completedRuns = 0;
30113016
const tasksToDelete: string[] = [];
3017+
const scheduledTaskIds = Array.from(this.taskRegistry.keys());
3018+
3019+
// Snapshot task ids so watcher-triggered reloads cannot repopulate the live map
3020+
// and revisit the same due task again inside a single scheduler cycle.
3021+
for (const taskId of scheduledTaskIds) {
3022+
const task = this.taskRegistry.get(taskId);
3023+
if (!task) {
3024+
continue;
3025+
}
30123026

3013-
for (const task of this.taskRegistry.values()) {
30143027
if (this.shouldSkipScheduledTask(task)) {
30153028
continue;
30163029
}

src/sqliteBootstrap.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ type SqlJsModule = {
4444
Database: new (data?: Uint8Array) => SqlJsDatabase;
4545
};
4646

47+
type SqliteAtomicWriteFs = Pick<typeof fs, "mkdirSync" | "writeFileSync" | "existsSync" | "renameSync" | "rmSync">;
48+
49+
let sqliteAtomicWriteFs: SqliteAtomicWriteFs = fs;
50+
51+
export function setSqliteAtomicWriteFsForTests(nextFs?: Partial<SqliteAtomicWriteFs> | null): void {
52+
sqliteAtomicWriteFs = nextFs ? { ...fs, ...nextFs } : fs;
53+
}
54+
4755
export type SqliteBootstrapResult = {
4856
databasePath: string;
4957
created: boolean;
@@ -207,41 +215,41 @@ function createSqliteSwapPath(databasePath: string, suffix: string): string {
207215
}
208216

209217
function persistSqliteDatabase(databasePath: string, db: SqlJsDatabase): void {
210-
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
218+
sqliteAtomicWriteFs.mkdirSync(path.dirname(databasePath), { recursive: true });
211219
const tempPath = createSqliteSwapPath(databasePath, "tmp");
212220
const backupPath = createSqliteSwapPath(databasePath, "bak");
213221
let movedExistingDatabase = false;
214222

215-
fs.writeFileSync(tempPath, Buffer.from(db.export()));
223+
sqliteAtomicWriteFs.writeFileSync(tempPath, Buffer.from(db.export()));
216224

217225
try {
218-
if (fs.existsSync(databasePath)) {
219-
fs.renameSync(databasePath, backupPath);
226+
if (sqliteAtomicWriteFs.existsSync(databasePath)) {
227+
sqliteAtomicWriteFs.renameSync(databasePath, backupPath);
220228
movedExistingDatabase = true;
221229
}
222230

223-
fs.renameSync(tempPath, databasePath);
231+
sqliteAtomicWriteFs.renameSync(tempPath, databasePath);
224232

225-
if (movedExistingDatabase && fs.existsSync(backupPath)) {
226-
fs.rmSync(backupPath, { force: true });
233+
if (movedExistingDatabase && sqliteAtomicWriteFs.existsSync(backupPath)) {
234+
sqliteAtomicWriteFs.rmSync(backupPath, { force: true });
227235
}
228236
} catch (error) {
229-
if (movedExistingDatabase && !fs.existsSync(databasePath) && fs.existsSync(backupPath)) {
237+
if (movedExistingDatabase && !sqliteAtomicWriteFs.existsSync(databasePath) && sqliteAtomicWriteFs.existsSync(backupPath)) {
230238
try {
231-
fs.renameSync(backupPath, databasePath);
239+
sqliteAtomicWriteFs.renameSync(backupPath, databasePath);
232240
} catch {
233241
// Best-effort restore; surface the original persistence error below.
234242
}
235243
}
236244

237245
throw error;
238246
} finally {
239-
if (fs.existsSync(tempPath)) {
240-
fs.rmSync(tempPath, { force: true });
247+
if (sqliteAtomicWriteFs.existsSync(tempPath)) {
248+
sqliteAtomicWriteFs.rmSync(tempPath, { force: true });
241249
}
242250

243-
if (fs.existsSync(backupPath)) {
244-
fs.rmSync(backupPath, { force: true });
251+
if (sqliteAtomicWriteFs.existsSync(backupPath)) {
252+
sqliteAtomicWriteFs.rmSync(backupPath, { force: true });
245253
}
246254
}
247255
}

src/test/suite/cockpitManager.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,63 @@ suite("ScheduleManager Overdue Task Tests", () => {
16401640
}
16411641
});
16421642

1643+
test("scheduler tick does not re-execute a recurring due task after reload repopulates the registry", async () => {
1644+
const workspaceRoot = fs.mkdtempSync(
1645+
path.join(os.tmpdir(), "copilot-scheduler-ws-tick-reload-revisit-"),
1646+
);
1647+
const storageRoot = fs.mkdtempSync(
1648+
path.join(os.tmpdir(), "copilot-scheduler-storage-tick-reload-revisit-"),
1649+
);
1650+
const restoreWs = overrideWorkspaceFolders(workspaceRoot);
1651+
let executeCount = 0;
1652+
1653+
try {
1654+
fs.mkdirSync(path.join(workspaceRoot, ".vscode"), { recursive: true });
1655+
fs.writeFileSync(
1656+
path.join(workspaceRoot, ".vscode", "scheduler.json"),
1657+
JSON.stringify(
1658+
{
1659+
tasks: [
1660+
{
1661+
id: "tick-reload-revisit",
1662+
name: "Tick reload revisit",
1663+
cron: "*/5 * * * *",
1664+
prompt: "run once per tick",
1665+
enabled: true,
1666+
createdAt: "2026-03-23T10:00:00.000Z",
1667+
updatedAt: "2026-03-23T10:00:00.000Z",
1668+
nextRun: "2026-03-23T10:05:00.000Z",
1669+
},
1670+
],
1671+
},
1672+
null,
1673+
2,
1674+
),
1675+
"utf8",
1676+
);
1677+
1678+
const manager = new ScheduleManager(createMockContext(storageRoot));
1679+
manager.setOnExecuteCallback(async () => {
1680+
executeCount += 1;
1681+
if (executeCount === 1) {
1682+
manager.reloadTasks();
1683+
}
1684+
});
1685+
1686+
await (manager as unknown as { evaluateAndRunDueTasks: () => Promise<void> }).evaluateAndRunDueTasks();
1687+
1688+
assert.strictEqual(executeCount, 1);
1689+
assert.ok(manager.getTask("tick-reload-revisit")?.lastRun instanceof Date);
1690+
assert.ok(
1691+
(manager.getTask("tick-reload-revisit")?.nextRun?.getTime() ?? 0)
1692+
> new Date("2026-03-23T10:05:00.000Z").getTime(),
1693+
);
1694+
} finally {
1695+
restoreWs();
1696+
removeTestPaths(workspaceRoot, storageRoot);
1697+
}
1698+
});
1699+
16431700
test("scheduler tick does not execute disabled tasks", async () => {
16441701
const workspaceRoot = fs.mkdtempSync(
16451702
path.join(os.tmpdir(), "copilot-scheduler-ws-disabled-due-"),

0 commit comments

Comments
 (0)