auq-mcp-server 2.2.2 → 2.4.0
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/README.md +82 -0
- package/dist/bin/auq.js +45 -39
- package/dist/bin/tui-app.js +78 -8
- package/dist/package.json +1 -1
- package/dist/src/__tests__/server.abort.test.js +214 -0
- package/dist/src/cli/commands/__tests__/answer.test.js +199 -0
- package/dist/src/cli/commands/__tests__/config.test.js +218 -0
- package/dist/src/cli/commands/__tests__/sessions.test.js +282 -0
- package/dist/src/cli/commands/answer.js +128 -0
- package/dist/src/cli/commands/config.js +263 -0
- package/dist/src/cli/commands/sessions.js +164 -0
- package/dist/src/cli/utils.js +95 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/types.js +4 -0
- package/dist/src/core/ask-user-questions.js +3 -2
- package/dist/src/i18n/locales/en.js +8 -1
- package/dist/src/i18n/locales/ko.js +8 -1
- package/dist/src/server.js +64 -11
- package/dist/src/session/SessionManager.js +69 -4
- package/dist/src/session/__tests__/SessionManager.test.js +65 -0
- package/dist/src/tui/ThemeProvider.js +2 -1
- package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
- package/dist/src/tui/components/ConfirmationDialog.js +5 -4
- package/dist/src/tui/components/Footer.js +24 -23
- package/dist/src/tui/components/ReviewScreen.js +2 -1
- package/dist/src/tui/components/SessionDots.js +33 -4
- package/dist/src/tui/components/SessionPicker.js +27 -18
- package/dist/src/tui/components/Spinner.js +19 -0
- package/dist/src/tui/components/StepperView.js +71 -7
- package/dist/src/tui/components/WaitingScreen.js +2 -1
- package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +134 -0
- package/dist/src/tui/components/__tests__/Footer.test.js +121 -0
- package/dist/src/tui/components/__tests__/ReviewScreen.test.js +89 -0
- package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
- package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +135 -0
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
- package/dist/src/tui/components/__tests__/WaitingScreen.test.js +60 -0
- package/dist/src/tui/constants/keybindings.js +40 -0
- package/dist/src/tui/session-watcher.js +50 -0
- package/dist/src/tui/themes/catppuccin-latte.js +7 -0
- package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
- package/dist/src/tui/themes/dark.js +7 -0
- package/dist/src/tui/themes/dracula.js +7 -0
- package/dist/src/tui/themes/github-dark.js +7 -0
- package/dist/src/tui/themes/github-light.js +7 -0
- package/dist/src/tui/themes/gruvbox-dark.js +7 -0
- package/dist/src/tui/themes/gruvbox-light.js +7 -0
- package/dist/src/tui/themes/light.js +7 -0
- package/dist/src/tui/themes/monokai.js +7 -0
- package/dist/src/tui/themes/nord.js +7 -0
- package/dist/src/tui/themes/one-dark.js +7 -0
- package/dist/src/tui/themes/rose-pine.js +7 -0
- package/dist/src/tui/themes/solarized-dark.js +7 -0
- package/dist/src/tui/themes/solarized-light.js +7 -0
- package/dist/src/tui/themes/tokyo-night.js +7 -0
- package/dist/src/tui/utils/__tests__/detectTheme.test.js +78 -0
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
- package/dist/src/tui/utils/staleDetection.js +51 -0
- package/package.json +1 -1
|
@@ -91,6 +91,7 @@ export const gruvboxLightTheme = {
|
|
|
91
91
|
successPillBg: "#ebdbb2", // light1 - light bg
|
|
92
92
|
error: "#9d0006",
|
|
93
93
|
info: "#076678",
|
|
94
|
+
warning: "#b57614",
|
|
94
95
|
border: "#d5c4a1",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const gruvboxLightTheme = {
|
|
|
105
106
|
untouched: "#a89984",
|
|
106
107
|
number: "#3c3836",
|
|
107
108
|
activeNumber: "#076678",
|
|
109
|
+
stale: "#b57614",
|
|
110
|
+
abandoned: "#9d0006",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#076678",
|
|
@@ -115,6 +118,10 @@ export const gruvboxLightTheme = {
|
|
|
115
118
|
highlightFg: "#79740e",
|
|
116
119
|
activeMark: "#076678",
|
|
117
120
|
progress: "#076678",
|
|
121
|
+
staleIcon: "#b57614",
|
|
122
|
+
staleText: "#b57614",
|
|
123
|
+
staleAge: "#b57614",
|
|
124
|
+
staleSubtitle: "#a89984",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -91,6 +91,7 @@ export const lightTheme = {
|
|
|
91
91
|
successPillBg: "#E6F9EE",
|
|
92
92
|
error: "#CF222E",
|
|
93
93
|
info: "#007EA7",
|
|
94
|
+
warning: "#B07D00",
|
|
94
95
|
border: "#D0D7DE",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const lightTheme = {
|
|
|
105
106
|
untouched: "#6E7781",
|
|
106
107
|
number: "#24292F",
|
|
107
108
|
activeNumber: "#007EA7",
|
|
109
|
+
stale: "#B07D00",
|
|
110
|
+
abandoned: "#CF222E",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#007EA7",
|
|
@@ -115,6 +118,10 @@ export const lightTheme = {
|
|
|
115
118
|
highlightFg: "#2DA44E",
|
|
116
119
|
activeMark: "#007EA7",
|
|
117
120
|
progress: "#007EA7",
|
|
121
|
+
staleIcon: "#B07D00",
|
|
122
|
+
staleText: "#B07D00",
|
|
123
|
+
staleAge: "#B07D00",
|
|
124
|
+
staleSubtitle: "#6E7781",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -92,6 +92,7 @@ export const monokaiTheme = {
|
|
|
92
92
|
successPillBg: "#272822",
|
|
93
93
|
error: "#F92672",
|
|
94
94
|
info: "#66D9EF",
|
|
95
|
+
warning: "#E6DB74",
|
|
95
96
|
border: "#49483E",
|
|
96
97
|
},
|
|
97
98
|
markdown: {
|
|
@@ -106,6 +107,8 @@ export const monokaiTheme = {
|
|
|
106
107
|
untouched: "#908B78",
|
|
107
108
|
number: "#F8F8F2",
|
|
108
109
|
activeNumber: "#66D9EF",
|
|
110
|
+
stale: "#E6DB74",
|
|
111
|
+
abandoned: "#F92672",
|
|
109
112
|
},
|
|
110
113
|
sessionPicker: {
|
|
111
114
|
border: "#66D9EF",
|
|
@@ -116,6 +119,10 @@ export const monokaiTheme = {
|
|
|
116
119
|
highlightFg: "#A6E22E",
|
|
117
120
|
activeMark: "#66D9EF",
|
|
118
121
|
progress: "#66D9EF",
|
|
122
|
+
staleIcon: "#E6DB74",
|
|
123
|
+
staleText: "#E6DB74",
|
|
124
|
+
staleAge: "#E6DB74",
|
|
125
|
+
staleSubtitle: "#908B78",
|
|
119
126
|
},
|
|
120
127
|
},
|
|
121
128
|
};
|
|
@@ -91,6 +91,7 @@ export const nordTheme = {
|
|
|
91
91
|
successPillBg: "#3b4252", // nord1
|
|
92
92
|
error: "#bf616a", // nord11
|
|
93
93
|
info: "#88c0d0", // nord8
|
|
94
|
+
warning: "#ebcb8b",
|
|
94
95
|
border: "#3b4252", // nord1
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const nordTheme = {
|
|
|
105
106
|
untouched: "#616E88",
|
|
106
107
|
number: "#eceff4",
|
|
107
108
|
activeNumber: "#88c0d0",
|
|
109
|
+
stale: "#ebcb8b",
|
|
110
|
+
abandoned: "#bf616a",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#88c0d0",
|
|
@@ -115,6 +118,10 @@ export const nordTheme = {
|
|
|
115
118
|
highlightFg: "#a3be8c",
|
|
116
119
|
activeMark: "#88c0d0",
|
|
117
120
|
progress: "#81a1c1",
|
|
121
|
+
staleIcon: "#ebcb8b",
|
|
122
|
+
staleText: "#ebcb8b",
|
|
123
|
+
staleAge: "#ebcb8b",
|
|
124
|
+
staleSubtitle: "#616E88",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -91,6 +91,7 @@ export const oneDarkTheme = {
|
|
|
91
91
|
successPillBg: "#282c34",
|
|
92
92
|
error: "#e06c75",
|
|
93
93
|
info: "#61afef",
|
|
94
|
+
warning: "#d19a66",
|
|
94
95
|
border: "#3e4451",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const oneDarkTheme = {
|
|
|
105
106
|
untouched: "#767D8A",
|
|
106
107
|
number: "#abb2bf",
|
|
107
108
|
activeNumber: "#61afef",
|
|
109
|
+
stale: "#d19a66",
|
|
110
|
+
abandoned: "#e06c75",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#61afef",
|
|
@@ -115,6 +118,10 @@ export const oneDarkTheme = {
|
|
|
115
118
|
highlightFg: "#98c379",
|
|
116
119
|
activeMark: "#61afef",
|
|
117
120
|
progress: "#56b6c2",
|
|
121
|
+
staleIcon: "#d19a66",
|
|
122
|
+
staleText: "#d19a66",
|
|
123
|
+
staleAge: "#d19a66",
|
|
124
|
+
staleSubtitle: "#767D8A",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -92,6 +92,7 @@ export const rosePineTheme = {
|
|
|
92
92
|
successPillBg: "#191724",
|
|
93
93
|
error: "#eb6f92",
|
|
94
94
|
info: "#9ccfd8",
|
|
95
|
+
warning: "#f6c177",
|
|
95
96
|
border: "#26233a",
|
|
96
97
|
},
|
|
97
98
|
markdown: {
|
|
@@ -106,6 +107,8 @@ export const rosePineTheme = {
|
|
|
106
107
|
untouched: "#8884a0",
|
|
107
108
|
number: "#e0def4",
|
|
108
109
|
activeNumber: "#ebbcba",
|
|
110
|
+
stale: "#f6c177",
|
|
111
|
+
abandoned: "#eb6f92",
|
|
109
112
|
},
|
|
110
113
|
sessionPicker: {
|
|
111
114
|
border: "#ebbcba",
|
|
@@ -116,6 +119,10 @@ export const rosePineTheme = {
|
|
|
116
119
|
highlightFg: "#31748f",
|
|
117
120
|
activeMark: "#ebbcba",
|
|
118
121
|
progress: "#9ccfd8",
|
|
122
|
+
staleIcon: "#f6c177",
|
|
123
|
+
staleText: "#f6c177",
|
|
124
|
+
staleAge: "#f6c177",
|
|
125
|
+
staleSubtitle: "#8884a0",
|
|
119
126
|
},
|
|
120
127
|
},
|
|
121
128
|
};
|
|
@@ -91,6 +91,7 @@ export const solarizedDarkTheme = {
|
|
|
91
91
|
successPillBg: "#073642",
|
|
92
92
|
error: "#dc322f",
|
|
93
93
|
info: "#268bd2",
|
|
94
|
+
warning: "#b58900",
|
|
94
95
|
border: "#073642",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const solarizedDarkTheme = {
|
|
|
105
106
|
untouched: "#6c7c83",
|
|
106
107
|
number: "#839496",
|
|
107
108
|
activeNumber: "#268bd2",
|
|
109
|
+
stale: "#b58900",
|
|
110
|
+
abandoned: "#dc322f",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#268bd2",
|
|
@@ -115,6 +118,10 @@ export const solarizedDarkTheme = {
|
|
|
115
118
|
highlightFg: "#859900",
|
|
116
119
|
activeMark: "#268bd2",
|
|
117
120
|
progress: "#2aa198",
|
|
121
|
+
staleIcon: "#b58900",
|
|
122
|
+
staleText: "#b58900",
|
|
123
|
+
staleAge: "#b58900",
|
|
124
|
+
staleSubtitle: "#6c7c83",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -91,6 +91,7 @@ export const solarizedLightTheme = {
|
|
|
91
91
|
successPillBg: "#eee8d5",
|
|
92
92
|
error: "#dc322f",
|
|
93
93
|
info: "#268bd2",
|
|
94
|
+
warning: "#b58900",
|
|
94
95
|
border: "#eee8d5",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const solarizedLightTheme = {
|
|
|
105
106
|
untouched: "#a3b1b1",
|
|
106
107
|
number: "#657b83",
|
|
107
108
|
activeNumber: "#268bd2",
|
|
109
|
+
stale: "#b58900",
|
|
110
|
+
abandoned: "#dc322f",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#268bd2",
|
|
@@ -115,6 +118,10 @@ export const solarizedLightTheme = {
|
|
|
115
118
|
highlightFg: "#859900",
|
|
116
119
|
activeMark: "#268bd2",
|
|
117
120
|
progress: "#2aa198",
|
|
121
|
+
staleIcon: "#b58900",
|
|
122
|
+
staleText: "#b58900",
|
|
123
|
+
staleAge: "#b58900",
|
|
124
|
+
staleSubtitle: "#a3b1b1",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -91,6 +91,7 @@ export const tokyoNightTheme = {
|
|
|
91
91
|
successPillBg: "#1a1b26",
|
|
92
92
|
error: "#f7768e",
|
|
93
93
|
info: "#7aa2f7",
|
|
94
|
+
warning: "#e0af68",
|
|
94
95
|
border: "#24283b",
|
|
95
96
|
},
|
|
96
97
|
markdown: {
|
|
@@ -105,6 +106,8 @@ export const tokyoNightTheme = {
|
|
|
105
106
|
untouched: "#7078A3",
|
|
106
107
|
number: "#c0caf5",
|
|
107
108
|
activeNumber: "#7aa2f7",
|
|
109
|
+
stale: "#e0af68",
|
|
110
|
+
abandoned: "#f7768e",
|
|
108
111
|
},
|
|
109
112
|
sessionPicker: {
|
|
110
113
|
border: "#7aa2f7",
|
|
@@ -115,6 +118,10 @@ export const tokyoNightTheme = {
|
|
|
115
118
|
highlightFg: "#9ece6a",
|
|
116
119
|
activeMark: "#7aa2f7",
|
|
117
120
|
progress: "#7dcfff",
|
|
121
|
+
staleIcon: "#e0af68",
|
|
122
|
+
staleText: "#e0af68",
|
|
123
|
+
staleAge: "#e0af68",
|
|
124
|
+
staleSubtitle: "#7078A3",
|
|
118
125
|
},
|
|
119
126
|
},
|
|
120
127
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { detectSystemTheme, clearThemeCache } from "../detectTheme.js";
|
|
3
|
+
describe("detectSystemTheme", () => {
|
|
4
|
+
const originalEnv = process.env;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset environment and clear cache before each test
|
|
7
|
+
process.env = { ...originalEnv };
|
|
8
|
+
clearThemeCache();
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
process.env = originalEnv;
|
|
12
|
+
clearThemeCache();
|
|
13
|
+
});
|
|
14
|
+
describe("COLORFGBG detection", () => {
|
|
15
|
+
it("should detect dark theme when background is 0 (black)", () => {
|
|
16
|
+
process.env.COLORFGBG = "15;0";
|
|
17
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
18
|
+
});
|
|
19
|
+
it("should detect dark theme when background is 6", () => {
|
|
20
|
+
process.env.COLORFGBG = "7;6";
|
|
21
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
22
|
+
});
|
|
23
|
+
it("should detect light theme when background is 7 (white/light gray)", () => {
|
|
24
|
+
process.env.COLORFGBG = "0;7";
|
|
25
|
+
expect(detectSystemTheme()).toBe("light");
|
|
26
|
+
});
|
|
27
|
+
it("should detect light theme when background is 15 (bright white)", () => {
|
|
28
|
+
process.env.COLORFGBG = "0;15";
|
|
29
|
+
expect(detectSystemTheme()).toBe("light");
|
|
30
|
+
});
|
|
31
|
+
it("should handle three-part COLORFGBG format", () => {
|
|
32
|
+
// Some terminals use foreground;middle;background format
|
|
33
|
+
process.env.COLORFGBG = "15;default;0";
|
|
34
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
35
|
+
});
|
|
36
|
+
it("should handle three-part COLORFGBG with light background", () => {
|
|
37
|
+
process.env.COLORFGBG = "0;default;15";
|
|
38
|
+
expect(detectSystemTheme()).toBe("light");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("fallback behavior", () => {
|
|
42
|
+
it("should fallback to dark when COLORFGBG is not set", () => {
|
|
43
|
+
delete process.env.COLORFGBG;
|
|
44
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
45
|
+
});
|
|
46
|
+
it("should fallback to dark when COLORFGBG is invalid", () => {
|
|
47
|
+
process.env.COLORFGBG = "invalid";
|
|
48
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
49
|
+
});
|
|
50
|
+
it("should fallback to dark when COLORFGBG has invalid number", () => {
|
|
51
|
+
process.env.COLORFGBG = "15;abc";
|
|
52
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
53
|
+
});
|
|
54
|
+
it("should fallback to dark when COLORFGBG is empty", () => {
|
|
55
|
+
process.env.COLORFGBG = "";
|
|
56
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("caching", () => {
|
|
60
|
+
it("should cache the result", () => {
|
|
61
|
+
process.env.COLORFGBG = "15;0";
|
|
62
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
63
|
+
// Change the environment variable
|
|
64
|
+
process.env.COLORFGBG = "0;15";
|
|
65
|
+
// Should still return cached result
|
|
66
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
67
|
+
});
|
|
68
|
+
it("should return fresh result after cache is cleared", () => {
|
|
69
|
+
process.env.COLORFGBG = "15;0";
|
|
70
|
+
expect(detectSystemTheme()).toBe("dark");
|
|
71
|
+
// Clear cache and change environment
|
|
72
|
+
clearThemeCache();
|
|
73
|
+
process.env.COLORFGBG = "0;15";
|
|
74
|
+
// Should return new result
|
|
75
|
+
expect(detectSystemTheme()).toBe("light");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DEFAULT_GRACE_PERIOD, formatStaleToastMessage, isSessionAbandoned, isSessionStale, } from "../staleDetection.js";
|
|
3
|
+
describe("staleDetection", () => {
|
|
4
|
+
const now = new Date("2026-01-01T12:00:00.000Z");
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
vi.setSystemTime(now);
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
describe("isSessionStale", () => {
|
|
13
|
+
const TWO_HOURS = 7200000; // default staleThreshold from config
|
|
14
|
+
it("should return true when session is older than threshold", () => {
|
|
15
|
+
// Session created 3 hours ago, threshold is 2 hours
|
|
16
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
17
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it("should return false when session is younger than threshold", () => {
|
|
20
|
+
// Session created 1 hour ago, threshold is 2 hours
|
|
21
|
+
const oneHourAgo = now.getTime() - 1 * 3600000;
|
|
22
|
+
expect(isSessionStale(oneHourAgo, TWO_HOURS)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
it("should return false when session is exactly at threshold", () => {
|
|
25
|
+
// Edge case: exactly at threshold boundary (not greater than)
|
|
26
|
+
const exactlyAtThreshold = now.getTime() - TWO_HOURS;
|
|
27
|
+
expect(isSessionStale(exactlyAtThreshold, TWO_HOURS)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it("should return false when stale session has recent interaction within grace period", () => {
|
|
30
|
+
// Session created 3 hours ago (stale), but user interacted 10 minutes ago
|
|
31
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
32
|
+
const tenMinutesAgo = now.getTime() - 10 * 60000;
|
|
33
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS, tenMinutesAgo)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it("should return true when stale session has old interaction outside grace period", () => {
|
|
36
|
+
// Session created 3 hours ago, last interaction 45 minutes ago (outside 30min grace)
|
|
37
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
38
|
+
const fortyFiveMinutesAgo = now.getTime() - 45 * 60000;
|
|
39
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS, fortyFiveMinutesAgo)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it("should respect custom grace period", () => {
|
|
42
|
+
// Session created 3 hours ago, last interaction 45 minutes ago
|
|
43
|
+
// With a 1-hour custom grace period, should NOT be stale
|
|
44
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
45
|
+
const fortyFiveMinutesAgo = now.getTime() - 45 * 60000;
|
|
46
|
+
const oneHourGrace = 3600000;
|
|
47
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS, fortyFiveMinutesAgo, oneHourGrace)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("should use DEFAULT_GRACE_PERIOD when no grace period provided", () => {
|
|
50
|
+
// Interaction 29 minutes ago (within default 30-minute grace)
|
|
51
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
52
|
+
const twentyNineMinutesAgo = now.getTime() - 29 * 60000;
|
|
53
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS, twentyNineMinutesAgo)).toBe(false);
|
|
54
|
+
// Interaction 31 minutes ago (outside default 30-minute grace)
|
|
55
|
+
const thirtyOneMinutesAgo = now.getTime() - 31 * 60000;
|
|
56
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS, thirtyOneMinutesAgo)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("should handle undefined lastInteraction", () => {
|
|
59
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
60
|
+
// No interaction => stale if age exceeds threshold
|
|
61
|
+
expect(isSessionStale(threeHoursAgo, TWO_HOURS, undefined)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it("should handle zero threshold (always stale if age > 0)", () => {
|
|
64
|
+
const oneSecondAgo = now.getTime() - 1000;
|
|
65
|
+
expect(isSessionStale(oneSecondAgo, 0)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("isSessionAbandoned", () => {
|
|
69
|
+
it("should return true for 'abandoned' status", () => {
|
|
70
|
+
expect(isSessionAbandoned("abandoned")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it("should return false for 'pending' status", () => {
|
|
73
|
+
expect(isSessionAbandoned("pending")).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it("should return false for 'completed' status", () => {
|
|
76
|
+
expect(isSessionAbandoned("completed")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it("should return false for 'in-progress' status", () => {
|
|
79
|
+
expect(isSessionAbandoned("in-progress")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it("should return false for 'rejected' status", () => {
|
|
82
|
+
expect(isSessionAbandoned("rejected")).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it("should return false for 'timed_out' status", () => {
|
|
85
|
+
expect(isSessionAbandoned("timed_out")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it("should return false for empty string", () => {
|
|
88
|
+
expect(isSessionAbandoned("")).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("formatStaleToastMessage", () => {
|
|
92
|
+
it("should format hours correctly for a 3-hour-old session", () => {
|
|
93
|
+
const threeHoursAgo = now.getTime() - 3 * 3600000;
|
|
94
|
+
const message = formatStaleToastMessage("Test Session", threeHoursAgo);
|
|
95
|
+
expect(message).toBe('Session "Test Session" may be orphaned (created 3h ago)');
|
|
96
|
+
});
|
|
97
|
+
it("should show 0h for very recent sessions", () => {
|
|
98
|
+
const thirtyMinutesAgo = now.getTime() - 30 * 60000;
|
|
99
|
+
const message = formatStaleToastMessage("New Session", thirtyMinutesAgo);
|
|
100
|
+
expect(message).toBe('Session "New Session" may be orphaned (created 0h ago)');
|
|
101
|
+
});
|
|
102
|
+
it("should show large hour counts for old sessions", () => {
|
|
103
|
+
const twoDaysAgo = now.getTime() - 48 * 3600000;
|
|
104
|
+
const message = formatStaleToastMessage("Old Session", twoDaysAgo);
|
|
105
|
+
expect(message).toBe('Session "Old Session" may be orphaned (created 48h ago)');
|
|
106
|
+
});
|
|
107
|
+
it("should handle session title with special characters", () => {
|
|
108
|
+
const twoHoursAgo = now.getTime() - 2 * 3600000;
|
|
109
|
+
const message = formatStaleToastMessage("Session \"quoted\"", twoHoursAgo);
|
|
110
|
+
expect(message).toBe('Session "Session "quoted"" may be orphaned (created 2h ago)');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe("DEFAULT_GRACE_PERIOD", () => {
|
|
114
|
+
it("should be 30 minutes in milliseconds", () => {
|
|
115
|
+
expect(DEFAULT_GRACE_PERIOD).toBe(1800000);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stale Session Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for computing session staleness. No I/O or React dependencies.
|
|
5
|
+
* Design Decision 4: "Stale is computed flag, not persisted status"
|
|
6
|
+
* Design Decision 8: "Interaction resets stale check for that session"
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Default grace period in milliseconds (30 minutes).
|
|
10
|
+
* If a user interacted with a session within this window, it is not considered stale.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_GRACE_PERIOD = 1800000; // 30 minutes
|
|
13
|
+
/**
|
|
14
|
+
* Determine whether a session should be considered stale based on its age,
|
|
15
|
+
* a configurable threshold, and an optional last-interaction timestamp.
|
|
16
|
+
*
|
|
17
|
+
* @param requestTimestamp - When the session was originally created (epoch ms)
|
|
18
|
+
* @param staleThreshold - Maximum allowed age before a session is stale (ms)
|
|
19
|
+
* @param lastInteraction - Last time the user interacted with this session (epoch ms)
|
|
20
|
+
* @param gracePeriod - Time after an interaction during which the session is not stale (ms)
|
|
21
|
+
* @returns true if the session is stale
|
|
22
|
+
*/
|
|
23
|
+
export function isSessionStale(requestTimestamp, staleThreshold, lastInteraction, gracePeriod = DEFAULT_GRACE_PERIOD) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
// If the user interacted recently (within grace period), session is not stale
|
|
26
|
+
if (lastInteraction && now - lastInteraction < gracePeriod) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return now - requestTimestamp > staleThreshold;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check whether a session has been explicitly marked as abandoned
|
|
33
|
+
* (e.g. AI disconnected).
|
|
34
|
+
*
|
|
35
|
+
* @param status - The session's current status string
|
|
36
|
+
* @returns true if status is "abandoned"
|
|
37
|
+
*/
|
|
38
|
+
export function isSessionAbandoned(status) {
|
|
39
|
+
return status === "abandoned";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a human-readable toast message for a stale session.
|
|
43
|
+
*
|
|
44
|
+
* @param sessionTitle - Display name or ID of the session
|
|
45
|
+
* @param createdAtMs - Session creation time in epoch milliseconds
|
|
46
|
+
* @returns Formatted message string
|
|
47
|
+
*/
|
|
48
|
+
export function formatStaleToastMessage(sessionTitle, createdAtMs) {
|
|
49
|
+
const hoursAgo = Math.floor((Date.now() - createdAtMs) / 3600000);
|
|
50
|
+
return `Session "${sessionTitle}" may be orphaned (created ${hoursAgo}h ago)`;
|
|
51
|
+
}
|