@vibe80/vibe80 0.1.1
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/LICENSE +201 -0
- package/README.md +52 -0
- package/bin/vibe80.js +176 -0
- package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
- package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
- package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
- package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
- package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
- package/client/dist/assets/browser-e3WgtMs-.js +8 -0
- package/client/dist/assets/index-CgqGyssr.css +32 -0
- package/client/dist/assets/index-DnwKjoj7.js +706 -0
- package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
- package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +35 -0
- package/client/dist/index.html +14 -0
- package/client/index.html +16 -0
- package/client/package.json +34 -0
- package/client/public/favicon.ico +0 -0
- package/client/public/favicon.png +0 -0
- package/client/public/favicon.svg +35 -0
- package/client/public/pwa-192x192.png +0 -0
- package/client/public/pwa-512x512.png +0 -0
- package/client/src/App.jsx +3131 -0
- package/client/src/assets/logo_small.png +0 -0
- package/client/src/assets/vibe80_dark.svg +51 -0
- package/client/src/assets/vibe80_light.svg +50 -0
- package/client/src/components/Chat/ChatComposer.jsx +228 -0
- package/client/src/components/Chat/ChatMessages.jsx +811 -0
- package/client/src/components/Chat/ChatToolbar.jsx +109 -0
- package/client/src/components/Chat/useChatComposer.js +462 -0
- package/client/src/components/Diff/DiffPanel.jsx +129 -0
- package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
- package/client/src/components/Logs/LogsPanel.jsx +80 -0
- package/client/src/components/SessionGate/SessionGate.jsx +874 -0
- package/client/src/components/Settings/SettingsPanel.jsx +212 -0
- package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
- package/client/src/components/Topbar/Topbar.jsx +101 -0
- package/client/src/components/WorktreeTabs.css +419 -0
- package/client/src/components/WorktreeTabs.jsx +604 -0
- package/client/src/hooks/useAttachments.jsx +125 -0
- package/client/src/hooks/useBacklog.js +254 -0
- package/client/src/hooks/useChatClear.js +90 -0
- package/client/src/hooks/useChatCollapse.js +42 -0
- package/client/src/hooks/useChatCommands.js +294 -0
- package/client/src/hooks/useChatExport.js +144 -0
- package/client/src/hooks/useChatMessagesState.js +69 -0
- package/client/src/hooks/useChatSend.js +158 -0
- package/client/src/hooks/useChatSocket.js +1239 -0
- package/client/src/hooks/useDiffNavigation.js +19 -0
- package/client/src/hooks/useExplorerActions.js +1184 -0
- package/client/src/hooks/useGitIdentity.js +114 -0
- package/client/src/hooks/useLayoutMode.js +31 -0
- package/client/src/hooks/useLocalPreferences.js +131 -0
- package/client/src/hooks/useMessageSync.js +30 -0
- package/client/src/hooks/useNotifications.js +132 -0
- package/client/src/hooks/usePaneNavigation.js +67 -0
- package/client/src/hooks/usePanelState.js +13 -0
- package/client/src/hooks/useProviderSelection.js +70 -0
- package/client/src/hooks/useRepoBranchesModels.js +218 -0
- package/client/src/hooks/useRepoStatus.js +350 -0
- package/client/src/hooks/useRpcLogActions.js +19 -0
- package/client/src/hooks/useRpcLogView.js +58 -0
- package/client/src/hooks/useSessionHandoff.js +97 -0
- package/client/src/hooks/useSessionLifecycle.js +287 -0
- package/client/src/hooks/useSessionReset.js +63 -0
- package/client/src/hooks/useSessionResync.js +77 -0
- package/client/src/hooks/useTerminalSession.js +328 -0
- package/client/src/hooks/useToolbarExport.js +27 -0
- package/client/src/hooks/useTurnInterrupt.js +43 -0
- package/client/src/hooks/useVibe80Forms.js +128 -0
- package/client/src/hooks/useWorkspaceAuth.js +932 -0
- package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
- package/client/src/hooks/useWorktrees.js +396 -0
- package/client/src/i18n.jsx +87 -0
- package/client/src/index.css +5147 -0
- package/client/src/locales/en.json +37 -0
- package/client/src/locales/fr.json +321 -0
- package/client/src/main.jsx +16 -0
- package/client/vite.config.js +62 -0
- package/docs/api/asyncapi.json +1511 -0
- package/docs/api/openapi.json +3242 -0
- package/git_hooks/prepare-commit-msg +35 -0
- package/package.json +36 -0
- package/server/package.json +29 -0
- package/server/scripts/rotate-workspace-secret.js +101 -0
- package/server/src/claudeClient.js +454 -0
- package/server/src/clientEvents.js +594 -0
- package/server/src/clientFactory.js +164 -0
- package/server/src/codexClient.js +468 -0
- package/server/src/config.js +27 -0
- package/server/src/helpers.js +138 -0
- package/server/src/index.js +1641 -0
- package/server/src/middleware/auth.js +93 -0
- package/server/src/middleware/debug.js +89 -0
- package/server/src/middleware/errorTypes.js +60 -0
- package/server/src/providerLogger.js +60 -0
- package/server/src/routes/files.js +114 -0
- package/server/src/routes/git.js +183 -0
- package/server/src/routes/health.js +13 -0
- package/server/src/routes/sessions.js +407 -0
- package/server/src/routes/workspaces.js +296 -0
- package/server/src/routes/worktrees.js +993 -0
- package/server/src/runAs.js +458 -0
- package/server/src/runtimeStore.js +32 -0
- package/server/src/services/auth.js +157 -0
- package/server/src/services/claudeThreadDirectory.js +33 -0
- package/server/src/services/session.js +918 -0
- package/server/src/services/workspace.js +858 -0
- package/server/src/storage/index.js +17 -0
- package/server/src/storage/redis.js +412 -0
- package/server/src/storage/sqlite.js +649 -0
- package/server/src/worktreeManager.js +717 -0
- package/server/tests/README.md +13 -0
- package/server/tests/factories/workspaceFactory.js +13 -0
- package/server/tests/fixtures/workspaceCredentials.json +4 -0
- package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
- package/server/tests/setup/env.js +9 -0
- package/server/tests/unit/helpers.test.js +95 -0
- package/server/tests/unit/services/auth.test.js +181 -0
- package/server/tests/unit/services/workspace.test.js +115 -0
- package/server/vitest.config.js +23 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("docker-names", () => ({
|
|
4
|
+
default: {
|
|
5
|
+
getRandomName: vi.fn(() => "calm-turing"),
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
classifySessionCreationError,
|
|
11
|
+
createDebugId,
|
|
12
|
+
createMessageId,
|
|
13
|
+
formatDebugPayload,
|
|
14
|
+
generateId,
|
|
15
|
+
generateRefreshToken,
|
|
16
|
+
generateSessionName,
|
|
17
|
+
getSessionTmpDir,
|
|
18
|
+
hashRefreshToken,
|
|
19
|
+
parseCommandArgs,
|
|
20
|
+
sanitizeFilename,
|
|
21
|
+
} from "../../src/helpers.js";
|
|
22
|
+
|
|
23
|
+
describe("helpers", () => {
|
|
24
|
+
it("generateId préfixe correctement un id hexadécimal", () => {
|
|
25
|
+
const id = generateId("w");
|
|
26
|
+
expect(id).toMatch(/^w[0-9a-f]{24}$/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("generateSessionName retourne un nom docker", () => {
|
|
30
|
+
expect(generateSessionName()).toBe("calm-turing");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("hashRefreshToken calcule un sha256 stable", () => {
|
|
34
|
+
expect(hashRefreshToken("abc")).toBe(
|
|
35
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("generateRefreshToken retourne 64 chars hex", () => {
|
|
40
|
+
expect(generateRefreshToken()).toMatch(/^[0-9a-f]{64}$/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("createMessageId et createDebugId retournent des identifiants non vides", () => {
|
|
44
|
+
expect(createMessageId().length).toBeGreaterThan(0);
|
|
45
|
+
expect(createDebugId().length).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("parseCommandArgs gère guillemets simples et doubles", () => {
|
|
49
|
+
expect(parseCommandArgs(`git commit -m "hello world" 'and more'`)).toEqual([
|
|
50
|
+
"git",
|
|
51
|
+
"commit",
|
|
52
|
+
"-m",
|
|
53
|
+
"hello world",
|
|
54
|
+
"and more",
|
|
55
|
+
]);
|
|
56
|
+
expect(parseCommandArgs("")).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("sanitizeFilename supprime le path", () => {
|
|
60
|
+
expect(sanitizeFilename("../../etc/passwd")).toBe("passwd");
|
|
61
|
+
expect(sanitizeFilename("")).toBe("attachment");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("getSessionTmpDir construit le chemin tmp", () => {
|
|
65
|
+
expect(getSessionTmpDir("/tmp/session-1")).toContain("/tmp/session-1");
|
|
66
|
+
expect(getSessionTmpDir("/tmp/session-1")).toMatch(/tmp$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("formatDebugPayload gère string/buffer/object/troncature", () => {
|
|
70
|
+
expect(formatDebugPayload("abc")).toBe("abc");
|
|
71
|
+
expect(formatDebugPayload(Buffer.from("abc", "utf8"))).toBe("abc");
|
|
72
|
+
expect(formatDebugPayload({ a: 1 })).toBe("{\"a\":1}");
|
|
73
|
+
expect(formatDebugPayload("abcdef", 3)).toBe("abc…(truncated)");
|
|
74
|
+
expect(formatDebugPayload(null)).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("classifySessionCreationError classe les erreurs connues", () => {
|
|
78
|
+
expect(
|
|
79
|
+
classifySessionCreationError(new Error("fatal: Authentication failed for repo"))
|
|
80
|
+
).toMatchObject({ status: 403 });
|
|
81
|
+
expect(
|
|
82
|
+
classifySessionCreationError(new Error("Permission denied (publickey)"))
|
|
83
|
+
).toMatchObject({ status: 403 });
|
|
84
|
+
expect(
|
|
85
|
+
classifySessionCreationError(new Error("repository not found"))
|
|
86
|
+
).toMatchObject({ status: 404 });
|
|
87
|
+
expect(
|
|
88
|
+
classifySessionCreationError(new Error("Could not resolve host github.com"))
|
|
89
|
+
).toMatchObject({ status: 400 });
|
|
90
|
+
expect(
|
|
91
|
+
classifySessionCreationError(new Error("Connection timed out"))
|
|
92
|
+
).toMatchObject({ status: 504 });
|
|
93
|
+
expect(classifySessionCreationError(new Error("boom"))).toMatchObject({ status: 500 });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const storageMock = vi.hoisted(() => ({
|
|
4
|
+
saveWorkspaceRefreshToken: vi.fn(),
|
|
5
|
+
rotateWorkspaceRefreshToken: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
const idSeq = vi.hoisted(() => ({ value: 0 }));
|
|
8
|
+
|
|
9
|
+
vi.mock("../../../src/storage/index.js", () => ({
|
|
10
|
+
default: storageMock,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("../../../src/middleware/auth.js", () => ({
|
|
14
|
+
createWorkspaceToken: vi.fn((workspaceId) => `token-${workspaceId}`),
|
|
15
|
+
accessTokenTtlSeconds: 3600,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("../../../src/helpers.js", () => ({
|
|
19
|
+
generateId: vi.fn((prefix) => {
|
|
20
|
+
idSeq.value += 1;
|
|
21
|
+
return `${prefix}_generated_${idSeq.value}`;
|
|
22
|
+
}),
|
|
23
|
+
hashRefreshToken: vi.fn((token) => `hash-${token}`),
|
|
24
|
+
generateRefreshToken: vi.fn(() => "refresh-token"),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe("services/auth", () => {
|
|
28
|
+
const loadAuthModule = async () => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
return import("../../../src/services/auth.js");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
idSeq.value = 0;
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
delete process.env.MONO_AUTH_TOKEN_TTL_MS;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("issueWorkspaceTokens crée un refresh token indépendant", async () => {
|
|
41
|
+
storageMock.saveWorkspaceRefreshToken.mockResolvedValueOnce();
|
|
42
|
+
|
|
43
|
+
const { issueWorkspaceTokens } = await loadAuthModule();
|
|
44
|
+
const result = await issueWorkspaceTokens("w123");
|
|
45
|
+
|
|
46
|
+
expect(result.workspaceToken).toBe("token-w123");
|
|
47
|
+
expect(result.refreshToken).toBe("refresh-token");
|
|
48
|
+
expect(result.expiresIn).toBe(3600);
|
|
49
|
+
expect(typeof result.refreshExpiresIn).toBe("number");
|
|
50
|
+
|
|
51
|
+
expect(storageMock.saveWorkspaceRefreshToken).toHaveBeenCalledTimes(1);
|
|
52
|
+
const call = storageMock.saveWorkspaceRefreshToken.mock.calls[0];
|
|
53
|
+
expect(call[0]).toBe("w123");
|
|
54
|
+
expect(call[1]).toBe("hash-refresh-token");
|
|
55
|
+
expect(call[3]).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("issueWorkspaceTokens n’ajoute pas de dépendance inter-token", async () => {
|
|
59
|
+
storageMock.saveWorkspaceRefreshToken.mockResolvedValueOnce();
|
|
60
|
+
const { issueWorkspaceTokens } = await loadAuthModule();
|
|
61
|
+
|
|
62
|
+
await issueWorkspaceTokens("w456");
|
|
63
|
+
|
|
64
|
+
const call = storageMock.saveWorkspaceRefreshToken.mock.calls[0];
|
|
65
|
+
expect(call[0]).toBe("w456");
|
|
66
|
+
expect(call[1]).toBe("hash-refresh-token");
|
|
67
|
+
expect(call[3]).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("rotateWorkspaceRefreshToken renvoie de nouveaux tokens si rotation valide", async () => {
|
|
71
|
+
storageMock.rotateWorkspaceRefreshToken.mockResolvedValueOnce({
|
|
72
|
+
ok: true,
|
|
73
|
+
workspaceId: "w123",
|
|
74
|
+
});
|
|
75
|
+
const { rotateWorkspaceRefreshToken } = await loadAuthModule();
|
|
76
|
+
|
|
77
|
+
const result = await rotateWorkspaceRefreshToken("refresh-token");
|
|
78
|
+
|
|
79
|
+
expect(result.ok).toBe(true);
|
|
80
|
+
expect(result.payload.workspaceToken).toBe("token-w123");
|
|
81
|
+
expect(result.payload.refreshToken).toBe("refresh-token");
|
|
82
|
+
expect(storageMock.rotateWorkspaceRefreshToken).toHaveBeenCalledWith(
|
|
83
|
+
"hash-refresh-token",
|
|
84
|
+
"hash-refresh-token",
|
|
85
|
+
expect.any(Number),
|
|
86
|
+
expect.any(Number)
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("rotateWorkspaceRefreshToken mappe les erreurs token", async () => {
|
|
91
|
+
storageMock.rotateWorkspaceRefreshToken.mockResolvedValueOnce({
|
|
92
|
+
ok: false,
|
|
93
|
+
code: "refresh_token_reused",
|
|
94
|
+
});
|
|
95
|
+
const { rotateWorkspaceRefreshToken } = await loadAuthModule();
|
|
96
|
+
const reused = await rotateWorkspaceRefreshToken("refresh-token");
|
|
97
|
+
expect(reused.ok).toBe(false);
|
|
98
|
+
expect(reused.status).toBe(401);
|
|
99
|
+
expect(reused.payload.code).toBe("refresh_token_reused");
|
|
100
|
+
|
|
101
|
+
storageMock.rotateWorkspaceRefreshToken.mockResolvedValueOnce({
|
|
102
|
+
ok: false,
|
|
103
|
+
code: "refresh_token_expired",
|
|
104
|
+
});
|
|
105
|
+
const expired = await rotateWorkspaceRefreshToken("refresh-token");
|
|
106
|
+
expect(expired.ok).toBe(false);
|
|
107
|
+
expect(expired.status).toBe(401);
|
|
108
|
+
expect(expired.payload.code).toBe("refresh_token_expired");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("createMonoAuthToken + consumeMonoAuthToken fonctionne puis invalide le token", async () => {
|
|
112
|
+
const { createMonoAuthToken, consumeMonoAuthToken } = await loadAuthModule();
|
|
113
|
+
const record = createMonoAuthToken("default");
|
|
114
|
+
|
|
115
|
+
const firstConsume = consumeMonoAuthToken(record.token);
|
|
116
|
+
expect(firstConsume).toEqual({ ok: true, workspaceId: "default" });
|
|
117
|
+
|
|
118
|
+
const secondConsume = consumeMonoAuthToken(record.token);
|
|
119
|
+
expect(secondConsume.ok).toBe(false);
|
|
120
|
+
expect(secondConsume.code).toBe("MONO_AUTH_TOKEN_INVALID");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("consumeMonoAuthToken retourne INVALID pour un token inconnu", async () => {
|
|
124
|
+
const { consumeMonoAuthToken } = await loadAuthModule();
|
|
125
|
+
const result = consumeMonoAuthToken("unknown-token");
|
|
126
|
+
expect(result).toEqual({ ok: false, code: "MONO_AUTH_TOKEN_INVALID" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("consumeMonoAuthToken retourne EXPIRED pour un token expiré", async () => {
|
|
130
|
+
vi.useFakeTimers();
|
|
131
|
+
process.env.MONO_AUTH_TOKEN_TTL_MS = "1";
|
|
132
|
+
const { createMonoAuthToken, consumeMonoAuthToken } = await loadAuthModule();
|
|
133
|
+
const record = createMonoAuthToken("default");
|
|
134
|
+
vi.advanceTimersByTime(10);
|
|
135
|
+
|
|
136
|
+
const result = consumeMonoAuthToken(record.token);
|
|
137
|
+
expect(result).toEqual({ ok: false, code: "MONO_AUTH_TOKEN_EXPIRED" });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("cleanupHandoffTokens supprime les tokens utilisés/expirés", async () => {
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
const {
|
|
143
|
+
createHandoffToken,
|
|
144
|
+
cleanupHandoffTokens,
|
|
145
|
+
handoffTokens,
|
|
146
|
+
} = await loadAuthModule();
|
|
147
|
+
handoffTokens.clear();
|
|
148
|
+
const expired = createHandoffToken({ sessionId: "s-exp", workspaceId: "w1" });
|
|
149
|
+
const used = createHandoffToken({ sessionId: "s-used", workspaceId: "w1" });
|
|
150
|
+
const willExpire = createHandoffToken({ sessionId: "s-exp", workspaceId: "w1" });
|
|
151
|
+
used.usedAt = Date.now();
|
|
152
|
+
vi.advanceTimersByTime(3 * 60 * 1000);
|
|
153
|
+
const fresh = createHandoffToken({ sessionId: "s-fresh", workspaceId: "w1" });
|
|
154
|
+
|
|
155
|
+
cleanupHandoffTokens();
|
|
156
|
+
|
|
157
|
+
expect(handoffTokens.has(expired.token)).toBe(false);
|
|
158
|
+
expect(handoffTokens.has(used.token)).toBe(false);
|
|
159
|
+
expect(handoffTokens.has(willExpire.token)).toBe(false);
|
|
160
|
+
expect(handoffTokens.has(fresh.token)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("cleanupMonoAuthTokens purge les tokens expirés", async () => {
|
|
164
|
+
vi.useFakeTimers();
|
|
165
|
+
process.env.MONO_AUTH_TOKEN_TTL_MS = "1";
|
|
166
|
+
const {
|
|
167
|
+
createMonoAuthToken,
|
|
168
|
+
cleanupMonoAuthTokens,
|
|
169
|
+
consumeMonoAuthToken,
|
|
170
|
+
} = await loadAuthModule();
|
|
171
|
+
const token = createMonoAuthToken("default").token;
|
|
172
|
+
vi.advanceTimersByTime(10);
|
|
173
|
+
|
|
174
|
+
cleanupMonoAuthTokens();
|
|
175
|
+
|
|
176
|
+
expect(consumeMonoAuthToken(token)).toEqual({
|
|
177
|
+
ok: false,
|
|
178
|
+
code: "MONO_AUTH_TOKEN_INVALID",
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const storageMock = vi.hoisted(() => ({
|
|
5
|
+
getWorkspace: vi.fn(),
|
|
6
|
+
saveWorkspace: vi.fn(),
|
|
7
|
+
appendWorkspaceAuditEvent: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("../../../src/storage/index.js", () => ({
|
|
11
|
+
default: storageMock,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("services/workspace", () => {
|
|
15
|
+
const loadWorkspaceModule = async () => {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
return import("../../../src/services/workspace.js");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
storageMock.getWorkspace.mockResolvedValue({
|
|
23
|
+
workspaceId: "w0123456789abcdef01234567",
|
|
24
|
+
providers: {
|
|
25
|
+
codex: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
auth: { type: "api_key", value: "x" },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
workspaceSecretHash: crypto.createHash("sha256").update("old").digest("hex"),
|
|
31
|
+
uid: 200001,
|
|
32
|
+
gid: 200001,
|
|
33
|
+
createdAt: 1,
|
|
34
|
+
updatedAt: 1,
|
|
35
|
+
});
|
|
36
|
+
storageMock.saveWorkspace.mockResolvedValue();
|
|
37
|
+
storageMock.appendWorkspaceAuditEvent.mockResolvedValue();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("rotateWorkspaceSecret rejette un workspaceId invalide", async () => {
|
|
41
|
+
const { rotateWorkspaceSecret } = await loadWorkspaceModule();
|
|
42
|
+
await expect(rotateWorkspaceSecret("invalid-id")).rejects.toThrow("Invalid workspaceId.");
|
|
43
|
+
expect(storageMock.getWorkspace).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rotateWorkspaceSecret met à jour le hash avec un secret fourni", async () => {
|
|
47
|
+
const { rotateWorkspaceSecret } = await loadWorkspaceModule();
|
|
48
|
+
const result = await rotateWorkspaceSecret("w0123456789abcdef01234567", {
|
|
49
|
+
workspaceSecret: "my-new-secret",
|
|
50
|
+
actor: "cli",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
workspaceId: "w0123456789abcdef01234567",
|
|
55
|
+
workspaceSecret: "my-new-secret",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(storageMock.saveWorkspace).toHaveBeenCalledTimes(1);
|
|
59
|
+
const savedPayload = storageMock.saveWorkspace.mock.calls[0][1];
|
|
60
|
+
expect(savedPayload.workspaceSecretHash).toBe(
|
|
61
|
+
crypto.createHash("sha256").update("my-new-secret").digest("hex")
|
|
62
|
+
);
|
|
63
|
+
expect(savedPayload.createdAt).toBe(1);
|
|
64
|
+
expect(savedPayload.updatedAt).toBeGreaterThan(1);
|
|
65
|
+
expect(storageMock.appendWorkspaceAuditEvent).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("rotateWorkspaceSecret génère un secret si absent", async () => {
|
|
69
|
+
const { rotateWorkspaceSecret } = await loadWorkspaceModule();
|
|
70
|
+
const result = await rotateWorkspaceSecret("w0123456789abcdef01234567");
|
|
71
|
+
|
|
72
|
+
expect(result.workspaceId).toBe("w0123456789abcdef01234567");
|
|
73
|
+
expect(result.workspaceSecret).toMatch(/^[0-9a-f]{64}$/);
|
|
74
|
+
const savedPayload = storageMock.saveWorkspace.mock.calls[0][1];
|
|
75
|
+
expect(savedPayload.workspaceSecretHash).toBe(
|
|
76
|
+
crypto.createHash("sha256").update(result.workspaceSecret).digest("hex")
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("verifyWorkspaceSecret retourne true/false selon le hash", async () => {
|
|
81
|
+
const { verifyWorkspaceSecret } = await loadWorkspaceModule();
|
|
82
|
+
await expect(
|
|
83
|
+
verifyWorkspaceSecret("w0123456789abcdef01234567", "old")
|
|
84
|
+
).resolves.toBe(true);
|
|
85
|
+
await expect(
|
|
86
|
+
verifyWorkspaceSecret("w0123456789abcdef01234567", "wrong")
|
|
87
|
+
).resolves.toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("validateProvidersConfig valide les cas principaux", async () => {
|
|
91
|
+
const { validateProvidersConfig } = await loadWorkspaceModule();
|
|
92
|
+
|
|
93
|
+
expect(validateProvidersConfig(null)).toBe("providers is required.");
|
|
94
|
+
expect(validateProvidersConfig({ unknown: { enabled: true } })).toBe(
|
|
95
|
+
"Unknown provider unknown."
|
|
96
|
+
);
|
|
97
|
+
expect(
|
|
98
|
+
validateProvidersConfig({
|
|
99
|
+
codex: { enabled: true, auth: null },
|
|
100
|
+
})
|
|
101
|
+
).toBe("Provider codex auth is required when enabled.");
|
|
102
|
+
expect(
|
|
103
|
+
validateProvidersConfig({
|
|
104
|
+
codex: { enabled: true, auth: { type: "setup_token", value: "x" } },
|
|
105
|
+
})
|
|
106
|
+
).toBe("Provider codex auth type setup_token is not supported.");
|
|
107
|
+
|
|
108
|
+
expect(
|
|
109
|
+
validateProvidersConfig({
|
|
110
|
+
codex: { enabled: true, auth: { type: "api_key", value: "x" } },
|
|
111
|
+
claude: { enabled: false, auth: null },
|
|
112
|
+
})
|
|
113
|
+
).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: "node",
|
|
6
|
+
globals: true,
|
|
7
|
+
include: ["tests/**/*.test.js"],
|
|
8
|
+
setupFiles: ["tests/setup/env.js"],
|
|
9
|
+
clearMocks: true,
|
|
10
|
+
restoreMocks: true,
|
|
11
|
+
},
|
|
12
|
+
coverage: {
|
|
13
|
+
provider: "v8",
|
|
14
|
+
reporter: ["text", "html"],
|
|
15
|
+
reportsDirectory: "./coverage",
|
|
16
|
+
thresholds: {
|
|
17
|
+
lines: 50,
|
|
18
|
+
functions: 50,
|
|
19
|
+
branches: 40,
|
|
20
|
+
statements: 50,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|