github-issue-tower-defence-management 1.143.1 → 1.143.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -9
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +2 -5
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js +2 -1
- package/bin/adapter/repositories/TmuxSilentSessionNotificationRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +1 -35
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -5
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +3 -8
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +0 -8
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.test.ts +35 -4
- package/src/adapter/repositories/TmuxSilentSessionNotificationRepository.ts +2 -1
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +0 -6
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +21 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +1 -1
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +21 -98
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +0 -44
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts +0 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/TmuxSilentSessionNotificationRepository.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +1 -3
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/src/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.test.ts +0 -127
- package/src/adapter/repositories/FileSystemSilentSessionNotifiedStateRepository.ts +0 -108
- package/src/domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository.ts +0 -10
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { FileSystemSilentSessionNotifiedStateRepository } from './FileSystemSilentSessionNotifiedStateRepository';
|
|
5
|
-
|
|
6
|
-
describe('FileSystemSilentSessionNotifiedStateRepository', () => {
|
|
7
|
-
let rootDirectory: string;
|
|
8
|
-
let stateFilePath: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
rootDirectory = fs.mkdtempSync(
|
|
12
|
-
path.join(os.tmpdir(), 'silent-session-notified-state-'),
|
|
13
|
-
);
|
|
14
|
-
stateFilePath = path.join(rootDirectory, 'silent-session-notified.json');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
fs.rmSync(rootDirectory, { force: true, recursive: true });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const sessionAlpha = 'https://github.com/HiromiShikata/repo/issues/100';
|
|
22
|
-
const sessionBravo = 'https://github.com/HiromiShikata/repo/issues/200';
|
|
23
|
-
|
|
24
|
-
it('round-trips a saved notified set so the next cycle reads it back', async () => {
|
|
25
|
-
const repository = new FileSystemSilentSessionNotifiedStateRepository(
|
|
26
|
-
stateFilePath,
|
|
27
|
-
);
|
|
28
|
-
const now = new Date('2026-06-26T00:00:00Z');
|
|
29
|
-
|
|
30
|
-
await repository.saveNotifiedSessionNames({
|
|
31
|
-
sessionNames: [sessionAlpha, sessionBravo],
|
|
32
|
-
now,
|
|
33
|
-
});
|
|
34
|
-
const loaded = await repository.loadRecentNotifiedSessionNames({
|
|
35
|
-
now: new Date('2026-06-26T00:01:00Z'),
|
|
36
|
-
recencyWindowSeconds: 15 * 60,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(loaded).toEqual(new Set([sessionAlpha, sessionBravo]));
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('returns an empty set when no state file exists yet', async () => {
|
|
43
|
-
const repository = new FileSystemSilentSessionNotifiedStateRepository(
|
|
44
|
-
stateFilePath,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
const loaded = await repository.loadRecentNotifiedSessionNames({
|
|
48
|
-
now: new Date('2026-06-26T00:00:00Z'),
|
|
49
|
-
recencyWindowSeconds: 15 * 60,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
expect(loaded).toEqual(new Set<string>());
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('excludes an entry older than the recency window when loading', async () => {
|
|
56
|
-
const repository = new FileSystemSilentSessionNotifiedStateRepository(
|
|
57
|
-
stateFilePath,
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
await repository.saveNotifiedSessionNames({
|
|
61
|
-
sessionNames: [sessionAlpha],
|
|
62
|
-
now: new Date('2026-06-26T00:00:00Z'),
|
|
63
|
-
});
|
|
64
|
-
const loaded = await repository.loadRecentNotifiedSessionNames({
|
|
65
|
-
now: new Date('2026-06-26T00:20:00Z'),
|
|
66
|
-
recencyWindowSeconds: 15 * 60,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(loaded).toEqual(new Set<string>());
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('overwrites the latch on save so a pruned session no longer lingers', async () => {
|
|
73
|
-
const repository = new FileSystemSilentSessionNotifiedStateRepository(
|
|
74
|
-
stateFilePath,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
await repository.saveNotifiedSessionNames({
|
|
78
|
-
sessionNames: [sessionAlpha, sessionBravo],
|
|
79
|
-
now: new Date('2026-06-26T00:00:00Z'),
|
|
80
|
-
});
|
|
81
|
-
await repository.saveNotifiedSessionNames({
|
|
82
|
-
sessionNames: [sessionAlpha],
|
|
83
|
-
now: new Date('2026-06-26T00:01:30Z'),
|
|
84
|
-
});
|
|
85
|
-
const loaded = await repository.loadRecentNotifiedSessionNames({
|
|
86
|
-
now: new Date('2026-06-26T00:02:00Z'),
|
|
87
|
-
recencyWindowSeconds: 15 * 60,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
expect(loaded).toEqual(new Set([sessionAlpha]));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('refreshes the recorded timestamp on each save so a continuously latched session stays within the window', async () => {
|
|
94
|
-
const repository = new FileSystemSilentSessionNotifiedStateRepository(
|
|
95
|
-
stateFilePath,
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
await repository.saveNotifiedSessionNames({
|
|
99
|
-
sessionNames: [sessionAlpha],
|
|
100
|
-
now: new Date('2026-06-26T00:00:00Z'),
|
|
101
|
-
});
|
|
102
|
-
await repository.saveNotifiedSessionNames({
|
|
103
|
-
sessionNames: [sessionAlpha],
|
|
104
|
-
now: new Date('2026-06-26T00:14:00Z'),
|
|
105
|
-
});
|
|
106
|
-
const loaded = await repository.loadRecentNotifiedSessionNames({
|
|
107
|
-
now: new Date('2026-06-26T00:20:00Z'),
|
|
108
|
-
recencyWindowSeconds: 15 * 60,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(loaded).toEqual(new Set([sessionAlpha]));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('returns an empty set when the state file is not valid JSON', async () => {
|
|
115
|
-
fs.writeFileSync(stateFilePath, 'not json');
|
|
116
|
-
const repository = new FileSystemSilentSessionNotifiedStateRepository(
|
|
117
|
-
stateFilePath,
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const loaded = await repository.loadRecentNotifiedSessionNames({
|
|
121
|
-
now: new Date('2026-06-26T00:00:00Z'),
|
|
122
|
-
recencyWindowSeconds: 15 * 60,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
expect(loaded).toEqual(new Set<string>());
|
|
126
|
-
});
|
|
127
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { SilentSessionNotifiedStateRepository } from '../../domain/usecases/adapter-interfaces/SilentSessionNotifiedStateRepository';
|
|
5
|
-
|
|
6
|
-
type StoredNotifiedEntry = {
|
|
7
|
-
sessionName: string;
|
|
8
|
-
recordedEpochSeconds: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
12
|
-
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
-
|
|
14
|
-
const defaultStateFilePath = (): string => {
|
|
15
|
-
const base = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
16
|
-
return path.join(base, 'tdpm', 'silent-session-notified.json');
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Persists the fire-once latch: the set of session names that have already
|
|
20
|
-
// received the current silent-episode reminder. The use case re-writes the
|
|
21
|
-
// full latch each cycle with the current timestamp, keeping a session latched
|
|
22
|
-
// for as long as it stays a reminder candidate (a continuous silent episode
|
|
23
|
-
// produces exactly one reminder), and prunes a session the moment it stops
|
|
24
|
-
// being a candidate so a later re-qualification fires again. The save
|
|
25
|
-
// intentionally OVERWRITES rather than merges: the use case supplies the
|
|
26
|
-
// complete latch to persist, so a pruned session must not linger.
|
|
27
|
-
export class FileSystemSilentSessionNotifiedStateRepository implements SilentSessionNotifiedStateRepository {
|
|
28
|
-
constructor(
|
|
29
|
-
private readonly stateFilePath: string = defaultStateFilePath(),
|
|
30
|
-
) {}
|
|
31
|
-
|
|
32
|
-
loadRecentNotifiedSessionNames = async (params: {
|
|
33
|
-
now: Date;
|
|
34
|
-
recencyWindowSeconds: number;
|
|
35
|
-
}): Promise<Set<string>> => {
|
|
36
|
-
const nowEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
37
|
-
const oldestAllowedEpochSeconds =
|
|
38
|
-
nowEpochSeconds - params.recencyWindowSeconds;
|
|
39
|
-
const recentSessionNames = new Set<string>();
|
|
40
|
-
for (const entry of this.readNotifiedEntries()) {
|
|
41
|
-
if (entry.recordedEpochSeconds >= oldestAllowedEpochSeconds) {
|
|
42
|
-
recentSessionNames.add(entry.sessionName);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return recentSessionNames;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
saveNotifiedSessionNames = async (params: {
|
|
49
|
-
sessionNames: string[];
|
|
50
|
-
now: Date;
|
|
51
|
-
}): Promise<void> => {
|
|
52
|
-
const recordedEpochSeconds = Math.floor(params.now.getTime() / 1000);
|
|
53
|
-
const notifiedBySessionName = new Map<string, StoredNotifiedEntry>();
|
|
54
|
-
for (const sessionName of params.sessionNames) {
|
|
55
|
-
notifiedBySessionName.set(sessionName, {
|
|
56
|
-
sessionName,
|
|
57
|
-
recordedEpochSeconds,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
this.writeState(Array.from(notifiedBySessionName.values()));
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
private readNotifiedEntries = (): StoredNotifiedEntry[] => {
|
|
64
|
-
let raw: string;
|
|
65
|
-
try {
|
|
66
|
-
raw = fs.readFileSync(this.stateFilePath, 'utf8');
|
|
67
|
-
} catch {
|
|
68
|
-
return [];
|
|
69
|
-
}
|
|
70
|
-
let parsed: unknown;
|
|
71
|
-
try {
|
|
72
|
-
parsed = JSON.parse(raw);
|
|
73
|
-
} catch {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
if (!isRecord(parsed)) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
const storedEntries = parsed.notified;
|
|
80
|
-
if (!Array.isArray(storedEntries)) {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
const entries: StoredNotifiedEntry[] = [];
|
|
84
|
-
for (const storedEntry of storedEntries) {
|
|
85
|
-
if (!isRecord(storedEntry)) {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const sessionName = storedEntry.sessionName;
|
|
89
|
-
const recordedEpochSeconds = storedEntry.recordedEpochSeconds;
|
|
90
|
-
if (
|
|
91
|
-
typeof sessionName === 'string' &&
|
|
92
|
-
typeof recordedEpochSeconds === 'number' &&
|
|
93
|
-
Number.isFinite(recordedEpochSeconds)
|
|
94
|
-
) {
|
|
95
|
-
entries.push({ sessionName, recordedEpochSeconds });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return entries;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
private writeState = (notified: StoredNotifiedEntry[]): void => {
|
|
102
|
-
const directory = path.dirname(this.stateFilePath);
|
|
103
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
104
|
-
const temporaryPath = `${this.stateFilePath}.${process.pid}.tmp`;
|
|
105
|
-
fs.writeFileSync(temporaryPath, JSON.stringify({ notified }));
|
|
106
|
-
fs.renameSync(temporaryPath, this.stateFilePath);
|
|
107
|
-
};
|
|
108
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export interface SilentSessionNotifiedStateRepository {
|
|
2
|
-
loadRecentNotifiedSessionNames: (params: {
|
|
3
|
-
now: Date;
|
|
4
|
-
recencyWindowSeconds: number;
|
|
5
|
-
}) => Promise<Set<string>>;
|
|
6
|
-
saveNotifiedSessionNames: (params: {
|
|
7
|
-
sessionNames: string[];
|
|
8
|
-
now: Date;
|
|
9
|
-
}) => Promise<void>;
|
|
10
|
-
}
|