Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/adapters/webTools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe('webSearch — backend selection', () => {
vi.stubEnv('OPENSWARM_SEARXNG_KEY', 'secret-key');
expect(searchBackend()).toBe('searxng');
const f = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
expect((init?.headers as Record<string, string>)['X-VEGA-Key']).toBe('secret-key');
expect((init?.headers as Record<string, string> | undefined)?.['X-VEGA-Key']).toBe('secret-key');
return new Response(JSON.stringify({ results: [] }), { status: 200 });
});
vi.stubGlobal('fetch', f);
Expand Down
24 changes: 19 additions & 5 deletions src/automation/dailyReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ const WATERMARK_FILE = join(homedir(), '.openswarm', 'daily-reporter-watermark.j

function readWatermark(): string | null {
try {
if (existsSync(WATERMARK_FILE)) {
return JSON.parse(readFileSync(WATERMARK_FILE, 'utf8')).date as string;
if (!existsSync(WATERMARK_FILE)) return null;
const parsed = JSON.parse(readFileSync(WATERMARK_FILE, 'utf8')) as unknown;
if (
typeof parsed === 'object' &&
parsed !== null &&
'date' in parsed &&
typeof parsed.date === 'string'
) {
return parsed.date;
}
} catch { /* corrupt → treat as no watermark */ }
} catch {
// A corrupt watermark must never suppress a report.
}
return null;
}

Expand Down Expand Up @@ -125,6 +134,12 @@ export async function generateDailyReports(): Promise<void> {
return;
}

const today = new Date().toISOString().slice(0, 10);
if (readWatermark() === today) {
console.log(`[DailyReporter] Reports already completed for ${today}, skipping`);
return;
}

console.log('[DailyReporter] Generating daily reports...');

try {
Expand Down Expand Up @@ -171,7 +186,6 @@ export async function generateDailyReports(): Promise<void> {
// If any failed, the watermark stays at the previous value so the next
// run retries the same window instead of skipping it.
if (failCount === 0) {
const today = new Date().toISOString().slice(0, 10);
writeWatermark(today);
console.log(`[DailyReporter] Watermark persisted: ${today}`);
} else {
Expand Down Expand Up @@ -216,4 +230,4 @@ async function sendDiscordSummary(
} catch (err) {
console.error('[DailyReporter] Failed to send Discord summary:', err);
}
}
}
80 changes: 80 additions & 0 deletions src/automation/dailyReporter.watermark.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { existsSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import type { LinearClient, Project } from '@linear/sdk';
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';

const testState = vi.hoisted(() => ({
home: `/tmp/openswarm-daily-reporter-${process.pid}`,
postStatusUpdate: vi.fn(),
}));

vi.mock('node:os', async (importOriginal) => ({
...(await importOriginal<typeof import('node:os')>()),
homedir: () => testState.home,
}));

vi.mock('../linear/index.js', () => ({
postStatusUpdate: testState.postStatusUpdate,
}));

import {
generateDailyReports,
setLinearClient,
setTeamId,
} from './dailyReporter.js';

const watermarkFile = join(testState.home, '.openswarm', 'daily-reporter-watermark.json');

function configureReporter(): { team: ReturnType<typeof vi.fn> } {
const project = { id: 'project-1', name: 'Project One', state: 'started' } as Project;
const team = {
projects: vi.fn().mockResolvedValue({
nodes: [project],
pageInfo: { hasNextPage: false, endCursor: null },
}),
};
const client = { team: vi.fn().mockResolvedValue(team) } as unknown as LinearClient;

setLinearClient(client);
setTeamId('team-1');
return { team: client.team as unknown as ReturnType<typeof vi.fn> };
}

beforeEach(() => {
rmSync(testState.home, { recursive: true, force: true });
testState.postStatusUpdate.mockReset();
});

afterAll(() => {
rmSync(testState.home, { recursive: true, force: true });
});

describe('daily reporter watermark', () => {
it('skips a duplicate run after a successful report', async () => {
testState.postStatusUpdate.mockResolvedValue(undefined);
const { team } = configureReporter();

await generateDailyReports();
await generateDailyReports();

expect(testState.postStatusUpdate).toHaveBeenCalledTimes(1);
expect(team).toHaveBeenCalledTimes(1);
expect(JSON.parse(readFileSync(watermarkFile, 'utf8'))).toEqual({
date: new Date().toISOString().slice(0, 10),
});
});

it('retries after a partial failure because no watermark is written', async () => {
testState.postStatusUpdate
.mockRejectedValueOnce(new Error('temporary Linear failure'))
.mockResolvedValueOnce(undefined);
configureReporter();

await generateDailyReports();
expect(existsSync(watermarkFile)).toBe(false);

await generateDailyReports();
expect(testState.postStatusUpdate).toHaveBeenCalledTimes(2);
expect(existsSync(watermarkFile)).toBe(true);
});
});
2 changes: 0 additions & 2 deletions src/support/web.tailscale.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
// literal never comes back.

import { describe, expect, it, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { isTailscaleAddress } from '../support/tailscaleNetwork';

async function withInterfaces(interfaces: Record<string, unknown[]>) {
vi.resetModules();
Expand Down
3 changes: 0 additions & 3 deletions web/static/js/diffPanel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function summarizeFiles(files) {
export class DiffPanel {
#el;
#fetchDiff;
#taskId = null;
/**
* Only the newest request may paint. A taskId comparison is not enough —
* re-opening the SAME session starts a second request with the same id, and
Expand All @@ -56,7 +55,6 @@ export class DiffPanel {
}

async load(taskId) {
this.#taskId = taskId;
const requestId = ++this.#requestId;
this.#message('Loading diff…');
let payload;
Expand All @@ -79,7 +77,6 @@ export class DiffPanel {
}

clear() {
this.#taskId = null;
// Nothing in flight may paint over a cleared panel either.
this.#requestId++;
this.#el.replaceChildren();
Expand Down
2 changes: 0 additions & 2 deletions web/static/js/sessionPanel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export class SessionPanel {
#diff;
#fetchLog;
#taskId = null;
#tab = 'transcript';
#seeded = new Set();

constructor(root, { store, transcripts, transcriptView, diffPanel, fetchLog }) {
Expand Down Expand Up @@ -131,7 +130,6 @@ export class SessionPanel {
}

#setTab(tab) {
this.#tab = tab;
for (const button of this.#root.querySelectorAll('.session-tab')) {
button.classList.toggle('active', button.dataset.tab === tab);
}
Expand Down
2 changes: 1 addition & 1 deletion web/static/js/themeBoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
theme = 'light';
}
} catch (_) { /* storage blocked — stay on the dark default */ }
} catch { /* storage blocked — stay on the dark default */ }
document.documentElement.setAttribute('data-theme', theme);
})();
2 changes: 1 addition & 1 deletion web/static/js/threadBoard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function mutationKey(prefix) {
async function request(fetchImpl, path, options = {}) {
const response = await fetchImpl(path, {
...options,
headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}) },
headers: { 'Content-Type': 'application/json', ...options.headers },
});
let body = null;
try { body = await response.json(); } catch { /* an error below still names the status */ }
Expand Down
14 changes: 7 additions & 7 deletions web/static/js/webToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
function readToken() {
try {
return window.sessionStorage.getItem(TOKEN_KEY) || '';
} catch (err) {
} catch {
return '';
}
}
Expand All @@ -52,7 +52,7 @@
try {
if (token) window.sessionStorage.setItem(TOKEN_KEY, token);
else window.sessionStorage.removeItem(TOKEN_KEY);
} catch (err) {
} catch {
// Non-fatal: the token still authorizes this page for its lifetime via
// the in-memory value the caller holds.
}
Expand All @@ -77,7 +77,7 @@
// one would send `/api/x` to another host while this still called it
// same-origin — attaching the token to a cross-origin request.
url = new URL(raw, document.baseURI || window.location.href);
} catch (err) {
} catch {
return null;
}
return url.origin === window.location.origin ? url.pathname : null;
Expand Down Expand Up @@ -264,7 +264,7 @@
var nativeFetch = scope.fetch.bind(scope);
try {
Object.defineProperty(scope, INSTALLED, { value: true, enumerable: false, configurable: true });
} catch (err) { scope[INSTALLED] = true; }
} catch { scope[INSTALLED] = true; }

scope.fetch = function (input, init) {
if (!isGatedRequest(input)) return nativeFetch(input, init);
Expand Down Expand Up @@ -344,7 +344,7 @@
// to have aborted it, the connection would otherwise stay open while
// the caller reconnects every three seconds, exhausting the per-host
// connection budget.
try { response.body.cancel(); } catch (err) { /* already released */ }
try { response.body.cancel(); } catch { /* already released */ }
return undefined;
}
self.readyState = 1;
Expand Down Expand Up @@ -413,7 +413,7 @@
this._closed = true;
this.readyState = 2;
if (this._controller) {
try { this._controller.abort(); } catch (err) { /* already aborted */ }
try { this._controller.abort(); } catch { /* already aborted */ }
}
};

Expand All @@ -438,7 +438,7 @@
scope.EventSource = makeTokenEventSource(scope, Native);
try {
Object.defineProperty(scope, ES_INSTALLED, { value: true, enumerable: false, configurable: true });
} catch (err) { scope[ES_INSTALLED] = true; }
} catch { scope[ES_INSTALLED] = true; }
}

// Exposed for tests and for pages that want to manage the token themselves.
Expand Down
Loading