@nightkatana/kronosys-app 1.0.0-beta.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 +81 -0
- package/app/api/action/route.ts +16 -0
- package/app/api/backup/route.ts +84 -0
- package/app/api/health/route.ts +22 -0
- package/app/api/state/route.ts +27 -0
- package/app/apple-icon.png +0 -0
- package/app/changelog/page.tsx +122 -0
- package/app/globals.css +210 -0
- package/app/guide/layout.tsx +11 -0
- package/app/guide/page.tsx +278 -0
- package/app/icon.png +0 -0
- package/app/layout.tsx +77 -0
- package/app/licenses/layout.tsx +11 -0
- package/app/licenses/page.tsx +246 -0
- package/app/manifest.ts +32 -0
- package/app/page.tsx +1610 -0
- package/app/reporting/page.tsx +2943 -0
- package/app/settings/layout.tsx +10 -0
- package/app/settings/page.tsx +3518 -0
- package/bin/kronosys.mjs +46 -0
- package/components/KronosysPackageVersionProvider.tsx +19 -0
- package/components/KronosysPayloadProvider.tsx +109 -0
- package/components/PwaRegister.tsx +25 -0
- package/components/SiteLegalFooter.tsx +21 -0
- package/components/ThemeProvider.tsx +78 -0
- package/components/dashboard/AppShellLiveSessionDrawer.tsx +394 -0
- package/components/dashboard/AppShellRouteNav.tsx +131 -0
- package/components/dashboard/AppVersionStamp.tsx +16 -0
- package/components/dashboard/DashboardCollapsibleSection.tsx +57 -0
- package/components/dashboard/DashboardColumnHintsBanner.tsx +159 -0
- package/components/dashboard/DashboardCommandCenter.tsx +470 -0
- package/components/dashboard/DashboardLangGateModal.tsx +118 -0
- package/components/dashboard/DashboardLoadingOverlay.tsx +42 -0
- package/components/dashboard/DashboardSimpleModal.tsx +337 -0
- package/components/dashboard/DashboardSuspenseFallback.tsx +52 -0
- package/components/dashboard/DashboardToastProvider.tsx +64 -0
- package/components/dashboard/DashboardTour.tsx +435 -0
- package/components/dashboard/DeferredDescriptionPopoverWrap.tsx +39 -0
- package/components/dashboard/DeleteSessionModal.tsx +130 -0
- package/components/dashboard/DescriptionTooltipPortaled.tsx +31 -0
- package/components/dashboard/GitIdentityQuickSetupModal.tsx +211 -0
- package/components/dashboard/HeaderIntegrationBadges.tsx +69 -0
- package/components/dashboard/InlineMetricHelpTrigger.tsx +102 -0
- package/components/dashboard/IssuePickerModal.tsx +168 -0
- package/components/dashboard/KronoFocusPanel.tsx +834 -0
- package/components/dashboard/KronosysDatetimePopoverField.tsx +357 -0
- package/components/dashboard/KronosysTimePopoverField.tsx +233 -0
- package/components/dashboard/LanguageMenu.tsx +123 -0
- package/components/dashboard/MongoMirrorSyncLine.tsx +57 -0
- package/components/dashboard/NewSessionScopeModal.tsx +410 -0
- package/components/dashboard/PageRefreshButton.tsx +130 -0
- package/components/dashboard/PlainHelpPopover.tsx +97 -0
- package/components/dashboard/ReportingPageToc.tsx +68 -0
- package/components/dashboard/ReportingTour.tsx +342 -0
- package/components/dashboard/SavedProjectPicker.tsx +92 -0
- package/components/dashboard/SavedTagPicker.tsx +115 -0
- package/components/dashboard/ScrollToTopFab.tsx +41 -0
- package/components/dashboard/SelectedSessionSidebarBlock.tsx +630 -0
- package/components/dashboard/SessionEndReasonEditor.tsx +114 -0
- package/components/dashboard/SessionListPanel.tsx +320 -0
- package/components/dashboard/SessionLocMetricsSection.tsx +128 -0
- package/components/dashboard/SettingsTagsProjectsSection.tsx +993 -0
- package/components/dashboard/SettingsTour.tsx +332 -0
- package/components/dashboard/TagPills.tsx +149 -0
- package/components/dashboard/TagsHelpTrigger.tsx +84 -0
- package/components/dashboard/TaskFocusPanel.tsx +1261 -0
- package/components/dashboard/TaskSessionLiveCard.tsx +832 -0
- package/components/dashboard/TaskSubtasksBlock.tsx +748 -0
- package/components/dashboard/ThemeToggle.test.tsx +26 -0
- package/components/dashboard/ThemeToggle.tsx +36 -0
- package/components/dashboard/UserGuideBodyText.tsx +62 -0
- package/components/dashboard/WorkspaceGitRepoCard.tsx +191 -0
- package/components/dashboard/taskFieldStyles.ts +139 -0
- package/components/dashboard/useAnchoredFloatingPortalStyle.ts +71 -0
- package/components/dashboard/useDescriptionPopoverAfterMs.ts +220 -0
- package/components/dashboard/useKronoFocusLiveSeconds.ts +36 -0
- package/components/dashboard/useSmoothStopwatchMs.ts +25 -0
- package/lib/appShellHeaderClasses.ts +12 -0
- package/lib/backupCsvExport.test.ts +149 -0
- package/lib/backupCsvExport.ts +392 -0
- package/lib/changelogCopy.ts +34 -0
- package/lib/concurrentTaskStartPreference.ts +29 -0
- package/lib/dashboardClockFormat.ts +13 -0
- package/lib/dashboardColumnChrome.ts +3 -0
- package/lib/dashboardColumnHintsStorage.ts +57 -0
- package/lib/dashboardCopy.ts +1831 -0
- package/lib/dashboardDetachedUrlHintStorage.ts +24 -0
- package/lib/dashboardGitIdentityBannerStorage.ts +36 -0
- package/lib/dashboardLangStorage.ts +72 -0
- package/lib/dashboardQuickSearch.ts +476 -0
- package/lib/dashboardQuickSearchQuery.test.ts +63 -0
- package/lib/dashboardQuickSearchQuery.ts +179 -0
- package/lib/dashboardSessionNav.ts +33 -0
- package/lib/dashboardShortcuts.ts +268 -0
- package/lib/dashboardTimeZone.ts +91 -0
- package/lib/dashboardTourStorage.ts +68 -0
- package/lib/dataDir.test.ts +87 -0
- package/lib/dataDir.ts +83 -0
- package/lib/devDataPreferenceFile.ts +55 -0
- package/lib/devDataRuntimeInfo.ts +34 -0
- package/lib/formatIsoShort.test.ts +46 -0
- package/lib/formatIsoShort.ts +29 -0
- package/lib/generatedUserChangelog.ts +34 -0
- package/lib/gitlabIssueSearch.ts +8 -0
- package/lib/kronoFocusDurationHistory.ts +71 -0
- package/lib/kronoFocusRhythm.test.ts +130 -0
- package/lib/kronoFocusRhythm.ts +46 -0
- package/lib/kronoFocusTimerUrgency.test.ts +74 -0
- package/lib/kronoFocusTimerUrgency.ts +24 -0
- package/lib/kronosysApi.ts +143 -0
- package/lib/legacyEditorPayloadKeys.ts +52 -0
- package/lib/legacyKronoFocusStorageKeys.test.ts +29 -0
- package/lib/legacyKronoFocusStorageKeys.ts +32 -0
- package/lib/licensesCopy.ts +128 -0
- package/lib/openPlainTextInNewTab.ts +49 -0
- package/lib/readKronosysPackageVersion.ts +10 -0
- package/lib/reportingAggregate.test.ts +325 -0
- package/lib/reportingAggregate.ts +819 -0
- package/lib/reportingDatePresets.ts +41 -0
- package/lib/reportingMetricHelp.ts +430 -0
- package/lib/reportingNonFinalIndicators.test.ts +157 -0
- package/lib/reportingNonFinalIndicators.ts +102 -0
- package/lib/reportingStrings.ts +491 -0
- package/lib/reportingTagWeekBreakdown.test.ts +141 -0
- package/lib/reportingTagWeekBreakdown.ts +181 -0
- package/lib/reportingWeekLayout.test.ts +239 -0
- package/lib/reportingWeekLayout.ts +313 -0
- package/lib/sessionAssiduity.test.ts +25 -0
- package/lib/sessionAssiduity.ts +33 -0
- package/lib/sessionEndReason.ts +55 -0
- package/lib/sessionEndWarnings.test.ts +200 -0
- package/lib/sessionEndWarnings.ts +125 -0
- package/lib/sessionListMerge.test.ts +101 -0
- package/lib/sessionListMerge.ts +70 -0
- package/lib/sessionTaskSidebarStats.test.ts +24 -0
- package/lib/sessionTaskSidebarStats.ts +54 -0
- package/lib/settingsCopy.ts +1276 -0
- package/lib/taskParsing.test.ts +153 -0
- package/lib/taskParsing.ts +737 -0
- package/lib/theme.ts +15 -0
- package/lib/translucentButtonClasses.ts +34 -0
- package/lib/usageProfile.test.ts +84 -0
- package/lib/usageProfile.ts +52 -0
- package/lib/userGuideCopy.ts +464 -0
- package/lib/workspaceLocDefaults.ts +21 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +15 -0
- package/package.json +87 -0
- package/postcss.config.mjs +12 -0
- package/public/apple-icon.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.png +0 -0
- package/public/next.svg +1 -0
- package/public/sw.js +13 -0
- package/public/traceback.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/actionDispatch.test.ts +723 -0
- package/server/actionDispatch.ts +1476 -0
- package/server/actionTaskSession.test.ts +713 -0
- package/server/actionTaskSession.ts +717 -0
- package/server/db.ts +42 -0
- package/server/defaultCfg.ts +87 -0
- package/server/gitlabTokenStore.ts +34 -0
- package/server/kronoFocusHydrate.test.ts +142 -0
- package/server/kronoFocusHydrate.ts +69 -0
- package/server/kronoFocusMigrate.test.ts +53 -0
- package/server/kronoFocusMigrate.ts +78 -0
- package/server/mainTimerHydrate.test.ts +65 -0
- package/server/mainTimerHydrate.ts +53 -0
- package/server/payloadStore.test.ts +78 -0
- package/server/payloadStore.ts +83 -0
- package/server/sessionWallHydrate.test.ts +46 -0
- package/server/sessionWallHydrate.ts +88 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { normalizeProjectKey } from "@/lib/taskParsing";
|
|
8
|
+
|
|
9
|
+
import { resetSqliteConnection } from "./db";
|
|
10
|
+
import { dispatchKronosysAction } from "./actionDispatch";
|
|
11
|
+
import { readPayload, writePayload } from "./payloadStore";
|
|
12
|
+
import { MAIN_TIMER_SEGMENT_STARTED_AT } from "./actionTaskSession";
|
|
13
|
+
import { SESSION_WALL_SEGMENT_STARTED_AT } from "./sessionWallHydrate";
|
|
14
|
+
|
|
15
|
+
describe("dispatchKronosysAction (SQLite isolé)", () => {
|
|
16
|
+
let tmp: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tb-disp-"));
|
|
20
|
+
process.env.TRACE_DATA_DIR = tmp;
|
|
21
|
+
resetSqliteConnection();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
resetSqliteConnection();
|
|
26
|
+
delete process.env.TRACE_DATA_DIR;
|
|
27
|
+
delete process.env.GITLAB_TOKEN;
|
|
28
|
+
vi.unstubAllGlobals();
|
|
29
|
+
if (tmp && fs.existsSync(tmp)) {
|
|
30
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("deux startTask d’affilée : seule la dernière tâche a le minuteur parent actif, l’autre en pause", async () => {
|
|
35
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
36
|
+
await dispatchKronosysAction({ type: "startTask", name: "Première", tags: [] });
|
|
37
|
+
await dispatchKronosysAction({ type: "startTask", name: "Deuxième", tags: [] });
|
|
38
|
+
const p = readPayload();
|
|
39
|
+
const cur = p.current as Record<string, unknown>;
|
|
40
|
+
const act = (cur.activeTasks as Record<string, unknown>[]) ?? [];
|
|
41
|
+
const second = act.find((t) => t.name === "Deuxième");
|
|
42
|
+
const first = act.find((t) => t.name === "Première");
|
|
43
|
+
expect(second?.manualTaskTimerPaused).toBeFalsy();
|
|
44
|
+
expect(first?.manualTaskTimerPaused).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("setActiveSubtaskTimer : la tâche ciblée en marche, les autres en pause (minuteur parent)", async () => {
|
|
48
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
49
|
+
await dispatchKronosysAction({ type: "startTask", name: "A", tags: [] });
|
|
50
|
+
await dispatchKronosysAction({ type: "startTask", name: "B", tags: [] });
|
|
51
|
+
let p = readPayload();
|
|
52
|
+
const cur0 = p.current as Record<string, unknown>;
|
|
53
|
+
const tA0 = (cur0.activeTasks as Record<string, unknown>[]).find((t) => t.name === "A");
|
|
54
|
+
const tidA = String(tA0?.id ?? "");
|
|
55
|
+
await dispatchKronosysAction({ type: "addSubtask", taskId: tidA, title: "Point" });
|
|
56
|
+
p = readPayload();
|
|
57
|
+
const cur1 = p.current as Record<string, unknown>;
|
|
58
|
+
const tA1 = (cur1.activeTasks as Record<string, unknown>[]).find((t) => t.name === "A");
|
|
59
|
+
const st = (tA1?.subtasks as { id: string; title: string }[] | undefined)?.[0];
|
|
60
|
+
expect(st?.id).toBeDefined();
|
|
61
|
+
await dispatchKronosysAction({
|
|
62
|
+
type: "setActiveSubtaskTimer",
|
|
63
|
+
taskId: tidA,
|
|
64
|
+
subtaskId: st?.id,
|
|
65
|
+
});
|
|
66
|
+
p = readPayload();
|
|
67
|
+
const cur = p.current as Record<string, unknown>;
|
|
68
|
+
const tA = (cur.activeTasks as Record<string, unknown>[]).find((t) => t.name === "A");
|
|
69
|
+
const tB = (cur.activeTasks as Record<string, unknown>[]).find((t) => t.name === "B");
|
|
70
|
+
expect(tA?.manualTaskTimerPaused).toBeFalsy();
|
|
71
|
+
expect(tA?.activeSubtaskTimerId).toBe(st?.id);
|
|
72
|
+
expect(tB?.manualTaskTimerPaused).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("resumePausedTask : une seule tâche a le minuteur parent actif, les suivi de sous-tâches sont effacés", async () => {
|
|
76
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
77
|
+
await dispatchKronosysAction({ type: "startTask", name: "A", tags: [] });
|
|
78
|
+
await dispatchKronosysAction({ type: "startTask", name: "B", tags: [] });
|
|
79
|
+
const p0 = readPayload();
|
|
80
|
+
const cur0 = p0.current as Record<string, unknown>;
|
|
81
|
+
const tA0 = (cur0.activeTasks as Record<string, unknown>[]).find((t) => t.name === "A");
|
|
82
|
+
const tB0 = (cur0.activeTasks as Record<string, unknown>[]).find((t) => t.name === "B");
|
|
83
|
+
const tidA = String(tA0?.id ?? "");
|
|
84
|
+
const tidB = String(tB0?.id ?? "");
|
|
85
|
+
await dispatchKronosysAction({ type: "addSubtask", taskId: tidA, title: "P" });
|
|
86
|
+
const p1 = readPayload();
|
|
87
|
+
const cur1 = p1.current as Record<string, unknown>;
|
|
88
|
+
const st = (
|
|
89
|
+
(cur1.activeTasks as Record<string, unknown>[]).find((t) => t.name === "A")?.subtasks as
|
|
90
|
+
| { id: string }[]
|
|
91
|
+
| undefined
|
|
92
|
+
)?.[0];
|
|
93
|
+
await dispatchKronosysAction({ type: "setActiveSubtaskTimer", taskId: tidA, subtaskId: st?.id });
|
|
94
|
+
await dispatchKronosysAction({ type: "resumePausedTask", taskId: tidB });
|
|
95
|
+
const p2 = readPayload();
|
|
96
|
+
const cur2 = p2.current as Record<string, unknown>;
|
|
97
|
+
const tA2 = (cur2.activeTasks as Record<string, unknown>[]).find((t) => t.name === "A");
|
|
98
|
+
const tB2 = (cur2.activeTasks as Record<string, unknown>[]).find((t) => t.name === "B");
|
|
99
|
+
expect(tA2?.activeSubtaskTimerId).toBeFalsy();
|
|
100
|
+
expect(tA2?.manualTaskTimerPaused).toBe(true);
|
|
101
|
+
expect(tB2?.manualTaskTimerPaused).toBeFalsy();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("démarre une tâche puis la termine sur la session live", async () => {
|
|
105
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
106
|
+
await dispatchKronosysAction({ type: "startTask", name: "Alpha", tags: ["t1"] });
|
|
107
|
+
let p = readPayload();
|
|
108
|
+
expect((p.knownTags as string[]).some((x) => String(x).toLowerCase() === "t1")).toBe(true);
|
|
109
|
+
const cur = p.current as Record<string, unknown> | undefined;
|
|
110
|
+
const tid = String((cur?.activeTask as Record<string, unknown> | undefined)?.id ?? "");
|
|
111
|
+
expect(tid.length).toBeGreaterThan(10);
|
|
112
|
+
|
|
113
|
+
await dispatchKronosysAction({ type: "finishTask", taskId: tid, shouldCommit: false });
|
|
114
|
+
p = readPayload();
|
|
115
|
+
const cur2 = p.current as Record<string, unknown>;
|
|
116
|
+
const tasks = cur2.tasks as Record<string, unknown>[];
|
|
117
|
+
expect(tasks.some((t) => String(t.id) === tid && t.isDone === true)).toBe(true);
|
|
118
|
+
const active = cur2.activeTasks as unknown[];
|
|
119
|
+
expect(Array.isArray(active) && active.length).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("startTask sans étiquette laisse tags vides si taskDefaultTagBucketEnabled est false", async () => {
|
|
123
|
+
await dispatchKronosysAction({
|
|
124
|
+
type: "updateKronosysSettings",
|
|
125
|
+
settings: { taskDefaultTagBucketEnabled: false },
|
|
126
|
+
});
|
|
127
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
128
|
+
await dispatchKronosysAction({ type: "startTask", name: "Sans tag", tags: [] });
|
|
129
|
+
const p = readPayload();
|
|
130
|
+
const at0 = (p.current as Record<string, unknown>).activeTask as Record<string, unknown> | undefined;
|
|
131
|
+
expect(at0?.tags).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("startTask enrichit knownProjects lorsqu’un @projet est fourni", async () => {
|
|
135
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
136
|
+
await dispatchKronosysAction({
|
|
137
|
+
type: "startTask",
|
|
138
|
+
name: "Tâche",
|
|
139
|
+
tags: [],
|
|
140
|
+
project: "nouveau-projet",
|
|
141
|
+
});
|
|
142
|
+
const p = readPayload();
|
|
143
|
+
const at0 = (p.current as Record<string, unknown>).activeTask as Record<string, unknown> | undefined;
|
|
144
|
+
expect(at0?.tags).toEqual(["default"]);
|
|
145
|
+
expect((p.knownProjects as string[]).map((x) => normalizeProjectKey(String(x)).toLowerCase())).toContain(
|
|
146
|
+
"nouveau-projet"
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("updateTask enrichit knownTags lorsqu’une nouvelle étiquette est appliquée", async () => {
|
|
151
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
152
|
+
await dispatchKronosysAction({ type: "startTask", name: "Une", tags: ["a"] });
|
|
153
|
+
let p = readPayload();
|
|
154
|
+
const tid = String(
|
|
155
|
+
((p.current as Record<string, unknown>).activeTask as Record<string, unknown> | undefined)?.id ?? ""
|
|
156
|
+
);
|
|
157
|
+
expect((p.knownTags as string[]).map((x) => x.toLowerCase())).toContain("a");
|
|
158
|
+
expect((p.knownTags as string[]).map((x) => x.toLowerCase())).not.toContain("nouveau");
|
|
159
|
+
await dispatchKronosysAction({ type: "updateTask", taskId: tid, name: "Une", tags: ["a", "nouveau"] });
|
|
160
|
+
p = readPayload();
|
|
161
|
+
expect((p.knownTags as string[]).map((x) => x.toLowerCase())).toContain("nouveau");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("updateTask enrichit knownProjects lorsqu’un projet est assigné", async () => {
|
|
165
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
166
|
+
await dispatchKronosysAction({ type: "startTask", name: "X", tags: [] });
|
|
167
|
+
let p = readPayload();
|
|
168
|
+
const tid = String(
|
|
169
|
+
((p.current as Record<string, unknown>).activeTask as Record<string, unknown> | undefined)?.id ?? ""
|
|
170
|
+
);
|
|
171
|
+
expect(
|
|
172
|
+
((p.current as Record<string, unknown>).activeTask as Record<string, unknown> | undefined)?.tags
|
|
173
|
+
).toEqual(["default"]);
|
|
174
|
+
await dispatchKronosysAction({ type: "updateTask", taskId: tid, name: "X", project: "client-acme" });
|
|
175
|
+
p = readPayload();
|
|
176
|
+
expect(
|
|
177
|
+
(p.knownProjects as string[]).some(
|
|
178
|
+
(x) => normalizeProjectKey(String(x)).toLowerCase() === "client-acme"
|
|
179
|
+
)
|
|
180
|
+
).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("fetchRemoteIssues refuse sans jeton disponible", async () => {
|
|
184
|
+
readPayload();
|
|
185
|
+
const before = readPayload();
|
|
186
|
+
const res = await dispatchKronosysAction({ type: "fetchRemoteIssues", search: "x" });
|
|
187
|
+
expect(res.ok).toBe(true);
|
|
188
|
+
expect(res.result?.remoteIssuesError).toBeTruthy();
|
|
189
|
+
expect(res.result?.remoteIssues ?? []).toEqual([]);
|
|
190
|
+
expect(JSON.stringify(readPayload())).toBe(JSON.stringify(before));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("fetchRemoteIssues retourne des issues ouvertes (mock GitLab)", async () => {
|
|
194
|
+
readPayload();
|
|
195
|
+
await dispatchKronosysAction({ type: "setGitlabToken", token: "glpat-x" });
|
|
196
|
+
const fetchMock = vi.fn((input: RequestInfo | URL) => {
|
|
197
|
+
const url = String(input);
|
|
198
|
+
if (url.includes("/api/v4/search")) {
|
|
199
|
+
return Promise.resolve({
|
|
200
|
+
ok: true,
|
|
201
|
+
status: 200,
|
|
202
|
+
json: async () => [],
|
|
203
|
+
text: async () => "[]",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return Promise.resolve({
|
|
207
|
+
ok: true,
|
|
208
|
+
status: 200,
|
|
209
|
+
json: async () => [
|
|
210
|
+
{
|
|
211
|
+
id: 9001,
|
|
212
|
+
iid: 3,
|
|
213
|
+
title: "Issue A",
|
|
214
|
+
references: { full: "group/proj#3" },
|
|
215
|
+
web_url: "https://gitlab.example/group/proj/-/issues/3",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
text: async () => "[]",
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
222
|
+
const res = await dispatchKronosysAction({ type: "fetchRemoteIssues", search: "fix" });
|
|
223
|
+
expect(fetchMock.mock.calls.length).toBe(2);
|
|
224
|
+
expect(res.result?.remoteIssuesError).toBeUndefined();
|
|
225
|
+
const list = res.result?.remoteIssues as Array<{ title?: string; number: unknown; source?: string }> | undefined;
|
|
226
|
+
expect(Array.isArray(list)).toBe(true);
|
|
227
|
+
expect(list?.length).toBe(1);
|
|
228
|
+
expect(list?.[0]?.title).toBe("Issue A");
|
|
229
|
+
expect(list?.[0]?.number).toBe(3);
|
|
230
|
+
expect(list?.[0]?.source).toBe("group/proj#3");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("fetchRemoteIssues par numéro seul utilise iids[] (pas search)", async () => {
|
|
234
|
+
readPayload();
|
|
235
|
+
await dispatchKronosysAction({ type: "setGitlabToken", token: "glpat-x" });
|
|
236
|
+
const fetchMock = vi.fn((_input: RequestInfo | URL) =>
|
|
237
|
+
Promise.resolve({
|
|
238
|
+
ok: true,
|
|
239
|
+
status: 200,
|
|
240
|
+
json: async () => [
|
|
241
|
+
{
|
|
242
|
+
id: 55,
|
|
243
|
+
iid: 8,
|
|
244
|
+
project_id: 1,
|
|
245
|
+
title: "Num",
|
|
246
|
+
references: { full: "g/p#8" },
|
|
247
|
+
web_url: "https://gitlab.example/g/p/-/issues/8",
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
text: async () => "[]",
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
254
|
+
const res = await dispatchKronosysAction({ type: "fetchRemoteIssues", search: " #8 " });
|
|
255
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
256
|
+
const calledUrl = String(fetchMock.mock.calls[0][0]);
|
|
257
|
+
expect(calledUrl).toContain("iids");
|
|
258
|
+
expect(calledUrl).toMatch(/=8(?:&|$)/);
|
|
259
|
+
expect(calledUrl).not.toMatch(/[?&]search=/);
|
|
260
|
+
const list = res.result?.remoteIssues as Array<{ title?: string; number?: unknown }> | undefined;
|
|
261
|
+
expect(list?.length).toBe(1);
|
|
262
|
+
expect(list?.[0]?.title).toBe("Num");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("fetchRemoteIssues sans recherche n’appelle pas l’API GitLab", async () => {
|
|
266
|
+
readPayload();
|
|
267
|
+
await dispatchKronosysAction({ type: "setGitlabToken", token: "tok" });
|
|
268
|
+
const fetchMock = vi.fn();
|
|
269
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
270
|
+
const res = await dispatchKronosysAction({ type: "fetchRemoteIssues", search: "" });
|
|
271
|
+
expect(res.result?.remoteIssues).toEqual([]);
|
|
272
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("fetchRemoteIssues renvoie un message clair si GitLab dépasse le délai", async () => {
|
|
276
|
+
readPayload();
|
|
277
|
+
await dispatchKronosysAction({ type: "setGitlabToken", token: "tok" });
|
|
278
|
+
const timeoutErr = new Error("The operation was aborted due to timeout");
|
|
279
|
+
timeoutErr.name = "TimeoutError";
|
|
280
|
+
vi.stubGlobal("fetch", vi.fn(() => Promise.reject(timeoutErr)) as unknown as typeof fetch);
|
|
281
|
+
const resFr = await dispatchKronosysAction({ type: "fetchRemoteIssues", search: "ab", lang: "fr" });
|
|
282
|
+
expect(resFr.result?.remoteIssues ?? []).toEqual([]);
|
|
283
|
+
expect(String(resFr.result?.remoteIssuesError)).toContain("12");
|
|
284
|
+
expect(String(resFr.result?.remoteIssuesError)).toContain("GitLab");
|
|
285
|
+
const resEn = await dispatchKronosysAction({ type: "fetchRemoteIssues", search: "ab", lang: "en" });
|
|
286
|
+
expect(String(resEn.result?.remoteIssuesError)).toMatch(/GitLab/i);
|
|
287
|
+
expect(String(resEn.result?.remoteIssuesError)).toMatch(/12/);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("refreshWorkspaceCodeSnapshot enregistre un instantané minimal", async () => {
|
|
291
|
+
readPayload();
|
|
292
|
+
await dispatchKronosysAction({ type: "refreshWorkspaceCodeSnapshot" });
|
|
293
|
+
const p = readPayload();
|
|
294
|
+
const snap = p.workspaceCodeSnapshot as Record<string, unknown> | undefined;
|
|
295
|
+
expect(snap?.ok).toBe(true);
|
|
296
|
+
expect(snap?.totalLines).toBe(0);
|
|
297
|
+
expect(typeof snap?.workspaceFolder).toBe("string");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("setMongoDbConnectionUri active les drapeaux de configuration", async () => {
|
|
301
|
+
readPayload();
|
|
302
|
+
await dispatchKronosysAction({ type: "setMongoDbConnectionUri", uri: "mongodb://127.0.0.1:27017" });
|
|
303
|
+
const p = readPayload();
|
|
304
|
+
const cfg = p.cfg as Record<string, unknown>;
|
|
305
|
+
expect(cfg.mongodbUriConfigured).toBe(true);
|
|
306
|
+
expect(cfg.mongodbManualUriConfigured).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("clearHistory vide l’historique, l’identité Git, les stats Git et réactive l’écran d’accueil", async () => {
|
|
310
|
+
readPayload();
|
|
311
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
312
|
+
await dispatchKronosysAction({ type: "startTask", name: "A #alpha", tags: ["alpha"], project: "mon-projet" });
|
|
313
|
+
await dispatchKronosysAction({
|
|
314
|
+
type: "endLiveSession",
|
|
315
|
+
sessionEndReasonKind: "planned",
|
|
316
|
+
sessionEndReasonNote: "x",
|
|
317
|
+
});
|
|
318
|
+
let p = readPayload();
|
|
319
|
+
expect((p.history as unknown[]).length).toBeGreaterThanOrEqual(1);
|
|
320
|
+
expect((p.knownTags as string[]).length).toBeGreaterThan(0);
|
|
321
|
+
expect((p.knownProjects as string[]).length).toBeGreaterThan(0);
|
|
322
|
+
await dispatchKronosysAction({
|
|
323
|
+
type: "setGitIdentity",
|
|
324
|
+
gitUserName: "Ada L.",
|
|
325
|
+
gitUserEmail: "ada@example.com",
|
|
326
|
+
gitAccountLogin: "adal",
|
|
327
|
+
});
|
|
328
|
+
await dispatchKronosysAction({ type: "refreshWorkspaceCodeSnapshot" });
|
|
329
|
+
p = readPayload();
|
|
330
|
+
writePayload({
|
|
331
|
+
...p,
|
|
332
|
+
gitStats: { isGitRepo: true, commitsOnHead: 3 },
|
|
333
|
+
dismissArchiveSessionConfirm: true,
|
|
334
|
+
});
|
|
335
|
+
await dispatchKronosysAction({
|
|
336
|
+
type: "updateKronosysSettings",
|
|
337
|
+
settings: { showWelcomeOnStartup: false },
|
|
338
|
+
});
|
|
339
|
+
await dispatchKronosysAction({ type: "clearHistory" });
|
|
340
|
+
p = readPayload();
|
|
341
|
+
expect(p.history).toEqual([]);
|
|
342
|
+
expect(p.historyArchived).toEqual([]);
|
|
343
|
+
expect(p.current).toBeUndefined();
|
|
344
|
+
expect(p.knownTags).toEqual([]);
|
|
345
|
+
expect(p.knownProjects).toEqual([]);
|
|
346
|
+
expect(p.userKnownTags).toEqual([]);
|
|
347
|
+
expect(p.excludedSuggestionTags).toEqual([]);
|
|
348
|
+
expect(p.tagDescriptions).toEqual({});
|
|
349
|
+
expect(p.projectDescriptions).toEqual({});
|
|
350
|
+
expect(p.gitIdentity).toEqual({});
|
|
351
|
+
expect(p.gitStats).toBeUndefined();
|
|
352
|
+
expect(p.dismissArchiveSessionConfirm).toBeUndefined();
|
|
353
|
+
expect(p.workspaceCodeSnapshot).toBeUndefined();
|
|
354
|
+
expect((p.cfg as Record<string, unknown>).showWelcomeOnStartup).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("resetKronosysSettings rétablit les valeurs par défaut et conserve les drapeaux secrets", async () => {
|
|
358
|
+
readPayload();
|
|
359
|
+
await dispatchKronosysAction({
|
|
360
|
+
type: "updateKronosysSettings",
|
|
361
|
+
settings: {
|
|
362
|
+
flushIntervalSeconds: 99,
|
|
363
|
+
gitRemoteUrl: "https://example.com/repo.git",
|
|
364
|
+
gitlabTokenStored: true,
|
|
365
|
+
gitlabTokenFromEnv: false,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
await dispatchKronosysAction({ type: "setMongoDbConnectionUri", uri: "mongodb://127.0.0.1:27017" });
|
|
369
|
+
await dispatchKronosysAction({ type: "setMongoDbPassword", password: "secret" });
|
|
370
|
+
await dispatchKronosysAction({ type: "resetKronosysSettings" });
|
|
371
|
+
const cfg = readPayload().cfg as Record<string, unknown>;
|
|
372
|
+
expect(cfg.flushIntervalSeconds).toBe(15);
|
|
373
|
+
expect(cfg.gitRemoteUrl).toBe("");
|
|
374
|
+
expect(cfg.gitlabTokenStored).toBe(true);
|
|
375
|
+
expect(cfg.gitlabTokenFromEnv).toBe(false);
|
|
376
|
+
expect(cfg.mongodbUriConfigured).toBe(true);
|
|
377
|
+
expect(cfg.mongodbManualUriConfigured).toBe(true);
|
|
378
|
+
expect(cfg.mongodbPasswordConfigured).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("setGitIdentity enregistre nom, courriel et identifiant forge", async () => {
|
|
382
|
+
readPayload();
|
|
383
|
+
await dispatchKronosysAction({
|
|
384
|
+
type: "setGitIdentity",
|
|
385
|
+
gitUserName: "Ada L.",
|
|
386
|
+
gitUserEmail: "ada@example.com",
|
|
387
|
+
gitAccountLogin: "adal",
|
|
388
|
+
});
|
|
389
|
+
const p = readPayload();
|
|
390
|
+
const g = p.gitIdentity as Record<string, unknown> | undefined;
|
|
391
|
+
expect(g?.gitUserName).toBe("Ada L.");
|
|
392
|
+
expect(g?.gitUserEmail).toBe("ada@example.com");
|
|
393
|
+
expect(g?.gitAccountLogin).toBe("adal");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("endLiveSession enregistre la raison sur l’instantané d’historique", async () => {
|
|
397
|
+
readPayload();
|
|
398
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
399
|
+
await dispatchKronosysAction({
|
|
400
|
+
type: "endLiveSession",
|
|
401
|
+
sessionEndReasonKind: "planned",
|
|
402
|
+
sessionEndReasonNote: " Sprint fini ",
|
|
403
|
+
});
|
|
404
|
+
const p = readPayload();
|
|
405
|
+
expect(p.current).toBeUndefined();
|
|
406
|
+
const hist = p.history as Record<string, unknown>[];
|
|
407
|
+
expect(hist.length).toBeGreaterThanOrEqual(1);
|
|
408
|
+
const row = hist[0];
|
|
409
|
+
expect(row.sessionEndReasonKind).toBe("planned");
|
|
410
|
+
expect(row.sessionEndReasonNote).toBe("Sprint fini");
|
|
411
|
+
expect(typeof row.endAt).toBe("string");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("endLiveSession conserve les tâches terminées dans l’historique", async () => {
|
|
415
|
+
readPayload();
|
|
416
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
417
|
+
await dispatchKronosysAction({ type: "startTask", name: "Tâche visible", tags: [] });
|
|
418
|
+
const cur = readPayload().current as Record<string, unknown>;
|
|
419
|
+
const active = cur.activeTask as Record<string, unknown>;
|
|
420
|
+
await dispatchKronosysAction({
|
|
421
|
+
type: "finishTask",
|
|
422
|
+
taskId: active.id,
|
|
423
|
+
shouldCommit: false,
|
|
424
|
+
});
|
|
425
|
+
await dispatchKronosysAction({ type: "endLiveSession" });
|
|
426
|
+
const p = readPayload();
|
|
427
|
+
const hist = p.history as Record<string, unknown>[];
|
|
428
|
+
expect(p.current).toBeUndefined();
|
|
429
|
+
expect(hist.length).toBeGreaterThanOrEqual(1);
|
|
430
|
+
const tasks = hist[0].tasks as Record<string, unknown>[];
|
|
431
|
+
expect(tasks).toHaveLength(1);
|
|
432
|
+
expect(tasks[0].name).toBe("Tâche visible");
|
|
433
|
+
expect(tasks[0].isDone).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("endLiveSession conserve aussi les tâches non terminées dans l’instantané d’historique", async () => {
|
|
437
|
+
readPayload();
|
|
438
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
439
|
+
await dispatchKronosysAction({ type: "startTask", name: "Tâche non terminée", tags: [] });
|
|
440
|
+
await dispatchKronosysAction({ type: "endLiveSession" });
|
|
441
|
+
const p = readPayload();
|
|
442
|
+
const hist = p.history as Record<string, unknown>[];
|
|
443
|
+
expect(p.current).toBeUndefined();
|
|
444
|
+
expect(hist.length).toBeGreaterThanOrEqual(1);
|
|
445
|
+
const activeTasks = hist[0].activeTasks as Record<string, unknown>[];
|
|
446
|
+
expect(activeTasks).toHaveLength(1);
|
|
447
|
+
expect(activeTasks[0].name).toBe("Tâche non terminée");
|
|
448
|
+
expect(activeTasks[0].isDone).toBe(false);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("setSessionEndReason met à jour l’instantané dans l’historique", async () => {
|
|
452
|
+
readPayload();
|
|
453
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
454
|
+
await dispatchKronosysAction({
|
|
455
|
+
type: "endLiveSession",
|
|
456
|
+
sessionEndReasonKind: "planned",
|
|
457
|
+
sessionEndReasonNote: "Note A",
|
|
458
|
+
});
|
|
459
|
+
let p = readPayload();
|
|
460
|
+
let row = (p.history as Record<string, unknown>[])[0];
|
|
461
|
+
const sid = String(row.sessionId);
|
|
462
|
+
await dispatchKronosysAction({
|
|
463
|
+
type: "setSessionEndReason",
|
|
464
|
+
sessionId: sid,
|
|
465
|
+
sessionEndReasonKind: "early",
|
|
466
|
+
sessionEndReasonNote: " Note B ",
|
|
467
|
+
});
|
|
468
|
+
p = readPayload();
|
|
469
|
+
row = (p.history as Record<string, unknown>[])[0];
|
|
470
|
+
expect(row.sessionEndReasonKind).toBe("early");
|
|
471
|
+
expect(row.sessionEndReasonNote).toBe("Note B");
|
|
472
|
+
await dispatchKronosysAction({
|
|
473
|
+
type: "setSessionEndReason",
|
|
474
|
+
sessionId: sid,
|
|
475
|
+
sessionEndReasonKind: "",
|
|
476
|
+
sessionEndReasonNote: "",
|
|
477
|
+
});
|
|
478
|
+
p = readPayload();
|
|
479
|
+
row = (p.history as Record<string, unknown>[])[0];
|
|
480
|
+
expect(row.sessionEndReasonKind).toBeUndefined();
|
|
481
|
+
expect(row.sessionEndReasonNote).toBeUndefined();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("setGitlabToken réinitialise gitlabApiVerified", async () => {
|
|
485
|
+
readPayload();
|
|
486
|
+
await dispatchKronosysAction({ type: "setGitlabToken", token: "abc" });
|
|
487
|
+
let p = readPayload();
|
|
488
|
+
expect((p.cfg as Record<string, unknown>).gitlabTokenStored).toBe(true);
|
|
489
|
+
expect((p.cfg as Record<string, unknown>).gitlabApiVerified).toBe(false);
|
|
490
|
+
|
|
491
|
+
vi.stubGlobal(
|
|
492
|
+
"fetch",
|
|
493
|
+
vi.fn(async () => ({
|
|
494
|
+
ok: true,
|
|
495
|
+
status: 200,
|
|
496
|
+
text: async () => "{}",
|
|
497
|
+
json: async () => ({}),
|
|
498
|
+
})) as unknown as typeof fetch,
|
|
499
|
+
);
|
|
500
|
+
await dispatchKronosysAction({ type: "testGitlabConnection", token: "abc" });
|
|
501
|
+
p = readPayload();
|
|
502
|
+
expect((p.cfg as Record<string, unknown>).gitlabApiVerified).toBe(true);
|
|
503
|
+
|
|
504
|
+
await dispatchKronosysAction({ type: "setGitlabToken", token: "xyz" });
|
|
505
|
+
p = readPayload();
|
|
506
|
+
expect((p.cfg as Record<string, unknown>).gitlabApiVerified).toBe(false);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("testGitlabConnection refuse une URL d’instance invalide sans appeler fetch", async () => {
|
|
510
|
+
readPayload();
|
|
511
|
+
const fetchMock = vi.fn();
|
|
512
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
513
|
+
const res = await dispatchKronosysAction({
|
|
514
|
+
type: "testGitlabConnection",
|
|
515
|
+
token: "glpat-x",
|
|
516
|
+
gitlabApiBaseUrl: "%%%not-a-valid-host",
|
|
517
|
+
});
|
|
518
|
+
expect((res.result?.gitlabConnectionTest as any)?.outcome).toBe("failed");
|
|
519
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("testGitlabConnection sans jeton renvoie no_token", async () => {
|
|
523
|
+
readPayload();
|
|
524
|
+
const res = await dispatchKronosysAction({ type: "testGitlabConnection", token: "" });
|
|
525
|
+
expect((res.result?.gitlabConnectionTest as any)?.outcome).toBe("failed");
|
|
526
|
+
expect((res.result?.gitlabConnectionTest as any)?.reason).toBe("no_token");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("testGitlabConnection réessaie avec Bearer si PRIVATE-TOKEN renvoie 401", async () => {
|
|
530
|
+
readPayload();
|
|
531
|
+
const fetchMock = vi
|
|
532
|
+
.fn()
|
|
533
|
+
.mockResolvedValueOnce({
|
|
534
|
+
status: 401,
|
|
535
|
+
ok: false,
|
|
536
|
+
text: async () => '{"message":"401 Unauthorized"}',
|
|
537
|
+
json: async () => ({}),
|
|
538
|
+
})
|
|
539
|
+
.mockResolvedValueOnce({
|
|
540
|
+
status: 200,
|
|
541
|
+
ok: true,
|
|
542
|
+
text: async () => '{"id":1}',
|
|
543
|
+
json: async () => ({ id: 1 }),
|
|
544
|
+
});
|
|
545
|
+
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
|
546
|
+
const res = await dispatchKronosysAction({ type: "testGitlabConnection", token: "glpat-test" });
|
|
547
|
+
expect((res.result?.gitlabConnectionTest as any)?.outcome).toBe("connected");
|
|
548
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
549
|
+
const first = fetchMock.mock.calls[0]?.[1] as { headers?: Record<string, string> };
|
|
550
|
+
const second = fetchMock.mock.calls[1]?.[1] as { headers?: Record<string, string> };
|
|
551
|
+
expect(first?.headers?.["PRIVATE-TOKEN"]).toBe("glpat-test");
|
|
552
|
+
expect(second?.headers?.Authorization).toBe("Bearer glpat-test");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("testGitlabConnection utilise GITLAB_TOKEN quand gitlabTokenFromEnv", async () => {
|
|
556
|
+
readPayload();
|
|
557
|
+
process.env.GITLAB_TOKEN = "from-env";
|
|
558
|
+
await dispatchKronosysAction({
|
|
559
|
+
type: "updateKronosysSettings",
|
|
560
|
+
settings: { gitlabTokenFromEnv: true, gitlabTokenStored: false },
|
|
561
|
+
});
|
|
562
|
+
vi.stubGlobal(
|
|
563
|
+
"fetch",
|
|
564
|
+
vi.fn(async () => ({
|
|
565
|
+
ok: true,
|
|
566
|
+
status: 200,
|
|
567
|
+
text: async () => "{}",
|
|
568
|
+
})) as unknown as typeof fetch,
|
|
569
|
+
);
|
|
570
|
+
const res = await dispatchKronosysAction({ type: "testGitlabConnection", token: "" });
|
|
571
|
+
expect((res.result?.gitlabConnectionTest as any)?.outcome).toBe("connected");
|
|
572
|
+
expect(vi.mocked(fetch)).toHaveBeenCalled();
|
|
573
|
+
const p = readPayload();
|
|
574
|
+
expect((p.cfg as Record<string, unknown>).gitlabApiVerified).toBe(true);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("setKronoFocusDurations enregistre le rythme travail / pauses sur la session en cours", async () => {
|
|
578
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
579
|
+
await dispatchKronosysAction({
|
|
580
|
+
type: "setKronoFocusDurations",
|
|
581
|
+
workSeconds: 50 * 60,
|
|
582
|
+
shortBreakSeconds: 10 * 60,
|
|
583
|
+
longBreakSeconds: 20 * 60,
|
|
584
|
+
});
|
|
585
|
+
const p = readPayload();
|
|
586
|
+
const kf = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
|
|
587
|
+
expect(kf.workDurationSeconds).toBe(50 * 60);
|
|
588
|
+
expect(kf.shortBreakDurationSeconds).toBe(10 * 60);
|
|
589
|
+
expect(kf.longBreakDurationSeconds).toBe(20 * 60);
|
|
590
|
+
expect(kf.timeLeftSeconds).toBe(50 * 60);
|
|
591
|
+
expect(kf.mode).toBe("work");
|
|
592
|
+
expect(kf.status).toBe("idle");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("setTaskStartTime recale la durée d'une tâche active et réinitialise le segment live", async () => {
|
|
596
|
+
vi.useFakeTimers();
|
|
597
|
+
vi.setSystemTime(new Date("2026-04-30T11:00:00.000Z"));
|
|
598
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
599
|
+
await dispatchKronosysAction({ type: "startTask", name: "Timer", tags: [] });
|
|
600
|
+
let p = readPayload();
|
|
601
|
+
const cur = p.current as Record<string, unknown>;
|
|
602
|
+
const active = (cur.activeTasks as Record<string, unknown>[])[0];
|
|
603
|
+
const tid = String(active.id ?? "");
|
|
604
|
+
const newStart = "2026-04-30T10:30:00.000Z";
|
|
605
|
+
await dispatchKronosysAction({ type: "setTaskStartTime", taskId: tid, startTime: newStart });
|
|
606
|
+
p = readPayload();
|
|
607
|
+
const cur2 = p.current as Record<string, unknown>;
|
|
608
|
+
const active2 = (cur2.activeTasks as Record<string, unknown>[])[0];
|
|
609
|
+
expect(active2.startTime).toBe(newStart);
|
|
610
|
+
expect(active2.durationMs).toBe(30 * 60 * 1000);
|
|
611
|
+
expect(active2[MAIN_TIMER_SEGMENT_STARTED_AT]).toBe("2026-04-30T11:00:00.000Z");
|
|
612
|
+
vi.useRealTimers();
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it("setSessionStartTime recale la durée d'une session active et réinitialise le segment mural", async () => {
|
|
616
|
+
vi.useFakeTimers();
|
|
617
|
+
vi.setSystemTime(new Date("2026-04-30T11:00:00.000Z"));
|
|
618
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
619
|
+
const newStart = "2026-04-30T10:00:00.000Z";
|
|
620
|
+
await dispatchKronosysAction({ type: "setSessionStartTime", startAt: newStart });
|
|
621
|
+
const p = readPayload();
|
|
622
|
+
const cur = p.current as Record<string, unknown>;
|
|
623
|
+
expect(cur.startAt).toBe(newStart);
|
|
624
|
+
expect(cur.sessionDurationMinutes).toBe(60);
|
|
625
|
+
expect(cur[SESSION_WALL_SEGMENT_STARTED_AT]).toBe("2026-04-30T11:00:00.000Z");
|
|
626
|
+
vi.useRealTimers();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("setSessionStartTime recale la durée d'une session terminée dans l'historique", async () => {
|
|
630
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
631
|
+
await dispatchKronosysAction({ type: "endLiveSession" });
|
|
632
|
+
let p = readPayload();
|
|
633
|
+
const sid = String(((p.history as Record<string, unknown>[])[0] ?? {}).sessionId ?? "");
|
|
634
|
+
const rowBefore = (p.history as Record<string, unknown>[])[0] ?? {};
|
|
635
|
+
const endAt = String(rowBefore.endAt ?? "");
|
|
636
|
+
const endMs = Date.parse(endAt);
|
|
637
|
+
const startMs = endMs - 90 * 60 * 1000;
|
|
638
|
+
const newStart = new Date(startMs).toISOString();
|
|
639
|
+
await dispatchKronosysAction({ type: "setSessionStartTime", sessionId: sid, startAt: newStart });
|
|
640
|
+
p = readPayload();
|
|
641
|
+
const rowAfter = (p.history as Record<string, unknown>[]).find((h) => String(h.sessionId) === sid) ?? {};
|
|
642
|
+
expect(rowAfter.startAt).toBe(newStart);
|
|
643
|
+
expect(Math.round(Number(rowAfter.sessionDurationMinutes))).toBe(90);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("setTaskEndTime recale la durée d'une tâche terminée", async () => {
|
|
647
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
648
|
+
await dispatchKronosysAction({ type: "startTask", name: "Done", tags: [] });
|
|
649
|
+
let p = readPayload();
|
|
650
|
+
const cur = p.current as Record<string, unknown>;
|
|
651
|
+
const tid = String(((cur.activeTasks as Record<string, unknown>[])[0] ?? {}).id ?? "");
|
|
652
|
+
await dispatchKronosysAction({ type: "finishTask", taskId: tid, shouldCommit: false });
|
|
653
|
+
p = readPayload();
|
|
654
|
+
const cur2 = p.current as Record<string, unknown>;
|
|
655
|
+
const tasks = (cur2.tasks as Record<string, unknown>[]) ?? [];
|
|
656
|
+
const done = tasks.find((t) => String(t.id) === tid) ?? {};
|
|
657
|
+
const startIso = String(done.startTime ?? "");
|
|
658
|
+
const startMs = Date.parse(startIso);
|
|
659
|
+
const newEnd = new Date(startMs + 45 * 60 * 1000).toISOString();
|
|
660
|
+
await dispatchKronosysAction({ type: "setTaskEndTime", taskId: tid, endTime: newEnd });
|
|
661
|
+
p = readPayload();
|
|
662
|
+
const cur3 = p.current as Record<string, unknown>;
|
|
663
|
+
const tasks3 = (cur3.tasks as Record<string, unknown>[]) ?? [];
|
|
664
|
+
const done3 = tasks3.find((t) => String(t.id) === tid) ?? {};
|
|
665
|
+
expect(done3.endTime).toBe(newEnd);
|
|
666
|
+
expect(done3.durationMs).toBe(45 * 60 * 1000);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("setSessionEndTime recale la durée d'une session terminée dans l'historique", async () => {
|
|
670
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
671
|
+
await dispatchKronosysAction({ type: "endLiveSession" });
|
|
672
|
+
let p = readPayload();
|
|
673
|
+
const sid = String(((p.history as Record<string, unknown>[])[0] ?? {}).sessionId ?? "");
|
|
674
|
+
const row = (p.history as Record<string, unknown>[])[0] ?? {};
|
|
675
|
+
const startAt = String(row.startAt ?? "");
|
|
676
|
+
const startMs = Date.parse(startAt);
|
|
677
|
+
const newEnd = new Date(startMs + 120 * 60 * 1000).toISOString();
|
|
678
|
+
await dispatchKronosysAction({
|
|
679
|
+
type: "setSessionEndTime",
|
|
680
|
+
sessionId: sid,
|
|
681
|
+
endAt: newEnd,
|
|
682
|
+
});
|
|
683
|
+
p = readPayload();
|
|
684
|
+
const rowAfter = (p.history as Record<string, unknown>[]).find((h) => String(h.sessionId) === sid) ?? {};
|
|
685
|
+
expect(rowAfter.endAt).toBe(newEnd);
|
|
686
|
+
expect(Math.round(Number(rowAfter.sessionDurationMinutes))).toBe(120);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("newSession enregistre un horodatage de création en plus du UUID", async () => {
|
|
690
|
+
vi.useFakeTimers();
|
|
691
|
+
vi.setSystemTime(new Date("2026-04-24T14:36:00.000Z"));
|
|
692
|
+
try {
|
|
693
|
+
await dispatchKronosysAction({ type: "newSession", sessionScope: undefined });
|
|
694
|
+
const p = readPayload();
|
|
695
|
+
const cur = p.current as Record<string, unknown>;
|
|
696
|
+
expect(typeof cur.sessionId).toBe("string");
|
|
697
|
+
expect(cur.createdAt).toBe("2026-04-24T14:36:00.000Z");
|
|
698
|
+
expect(cur.startAt).toBe("2026-04-24T14:36:00.000Z");
|
|
699
|
+
const hist = p.history as Record<string, unknown>[];
|
|
700
|
+
expect(hist[0]?.createdAt).toBe("2026-04-24T14:36:00.000Z");
|
|
701
|
+
} finally {
|
|
702
|
+
vi.useRealTimers();
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("newSession enregistre scheduledStartAt et sessionStartOffsetMinutes si l’hôte fournit l’heure prévue", async () => {
|
|
707
|
+
vi.useFakeTimers();
|
|
708
|
+
vi.setSystemTime(new Date("2026-04-24T14:15:00.000Z"));
|
|
709
|
+
try {
|
|
710
|
+
await dispatchKronosysAction({
|
|
711
|
+
type: "newSession",
|
|
712
|
+
sessionScope: undefined,
|
|
713
|
+
scheduledStartAt: "2026-04-24T14:00:00.000Z",
|
|
714
|
+
} as Record<string, unknown>);
|
|
715
|
+
const p = readPayload();
|
|
716
|
+
const cur = p.current as Record<string, unknown>;
|
|
717
|
+
expect(cur.scheduledStartAt).toBe("2026-04-24T14:00:00.000Z");
|
|
718
|
+
expect(cur.sessionStartOffsetMinutes).toBe(15);
|
|
719
|
+
} finally {
|
|
720
|
+
vi.useRealTimers();
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
});
|