1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import { ProviderInstanceId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7
8import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
9import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
10import * as Layer from "effect/Layer";
11import { makeCursorCredentialStore } from "./credentialStore.ts";
12
13const layerHost = Layer.provideMerge(TestProviderHost.layer(), NodeServices.layer);
14
15const legacyCredentials = {
16 version: 1 as const,
17 backendUrl: "https://api.cursor.com",
18 apiKey: "test-only-legacy-key",
19 createdAtMs: 100,
20};
21// The SDK's FileCredentialStore writes pretty-printed JSON.
22const legacyFileText = `{
23 "version": 1,
24 "backendUrl": "https://api.cursor.com",
25 "apiKey": "test-only-legacy-key",
26 "createdAtMs": 100
27}`;
28
29it.effect.each([
30 {
31 name: "imports a legacy sign-in",
32 legacy: legacyFileText,
33 stored: undefined,
34 expected: "test-only-legacy-key",
35 },
36 {
37 name: "keeps a newer stored sign-in",
38 legacy: legacyFileText,
39 stored: legacyFileText.replace("test-only-legacy-key", "test-only-current-key"),
40 expected: "test-only-current-key",
41 },
42 {
43 name: "replaces a damaged stored sign-in",
44 legacy: legacyFileText,
45 stored: "damaged",
46 expected: "test-only-legacy-key",
47 },
48 {
49 name: "discards a damaged legacy file",
50 legacy: "damaged",
51 stored: undefined,
52 expected: undefined,
53 },
54])("$name and deletes the legacy file", ({ legacy, stored, expected }) =>
55 Effect.gen(function* () {
56 const fileSystem = yield* FileSystem.FileSystem;
57 const path = yield* Path.Path;
58 const legacyFile = path.join(yield* fileSystem.makeTempDirectoryScoped(), "cursor.json");
59 yield* fileSystem.writeFileString(legacyFile, legacy);
60 const host = yield* ProviderHost.ProviderHost;
61 const instanceId = ProviderInstanceId.make("personal");
62 if (stored !== undefined) {
63 yield* (yield* host.credentials("cursor", instanceId)).set(new TextEncoder().encode(stored));
64 }
65 const migrated = yield* makeCursorCredentialStore(instanceId, legacyFile);
66 assert.strictEqual((yield* Effect.tryPromise(() => migrated.store.load()))?.apiKey, expected);
67 assert.isFalse(yield* fileSystem.exists(legacyFile));
68 }).pipe(Effect.scoped, Effect.provide(layerHost)),
69);
70
71it.effect(
72 "restores SDK credentials in a new controller and keeps another account when signing out",
73 () =>
74 Effect.gen(function* () {
75 const host = yield* ProviderHost.ProviderHost;
76 const makeStore = (id: string) => makeCursorCredentialStore(ProviderInstanceId.make(id));
77 const personal = yield* makeStore("personal");
78 const work = yield* makeStore("work");
79 const credentials = {
80 version: 1 as const,
81 backendUrl: "https://api.cursor.com",
82 apiKey: "test-only-key",
83 createdAtMs: 100,
84 apiKeyExpiresAtMs: 1000,
85 email: "test@example.com",
86 };
87 yield* Effect.tryPromise(() => personal.store.save(credentials));
88 yield* Effect.tryPromise(() =>
89 work.store.save({ ...credentials, apiKey: "test-only-work-key" }),
90 );
91 const restored = yield* makeStore("personal");
92 assert.deepEqual(yield* Effect.tryPromise(() => restored.store.load()), credentials);
93 yield* (yield* host.credentials("cursor", "personal")).set(
94 new TextEncoder().encode("damaged credential"),
95 );
96 assert.isUndefined(yield* Effect.tryPromise(() => restored.store.load()));
97 yield* Effect.tryPromise(() => restored.store.save(credentials));
98 yield* Effect.tryPromise(() => restored.store.clear());
99 assert.isUndefined(yield* Effect.tryPromise(() => personal.store.load()));
100 assert.strictEqual(
101 (yield* Effect.tryPromise(() => work.store.load()))?.apiKey,
102 "test-only-work-key",
103 );
104 }).pipe(Effect.provide(layerHost)),
105);