Skip to content
Merged
4 changes: 3 additions & 1 deletion .agents/references/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ Use populated fixtures when the affected flow needs them. For an authorized snap
```bash
mkdir -p .t3/userdata
# Use a fresh destination; VACUUM INTO refuses to overwrite an existing file.
bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.pylon-code/userdata/state.sqlite', { readonly: true }).run(\"VACUUM INTO '.t3/userdata/state.sqlite'\")"
bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.pylon-code/userdata/statev2.sqlite', { readonly: true }).run(\"VACUUM INTO '.t3/userdata/statev2.sqlite'\")"
```

The server and `vp run migrate-dev-db` use `statev2.sqlite`. An install created before orchestration v2 may only have `state.sqlite`; `migrate-dev-db` then stops safely with a source-missing error, so pass `--source <path>` explicitly.

A plain `cp` is only safe when no server has the source open, and must bring the `-wal` and `-shm` siblings along. A live file copy is a corrupt copy.

- Bring `secrets` and `settings.json` only if the flow under test needs them.
Expand Down
14 changes: 6 additions & 8 deletions apps/desktop/scripts/browser-secret-native.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,19 @@ describe.skipIf(hostPlatform !== "linux")("bundled libsecret helper", () => {
expect(result.stderr.length).toBe(0);
});

for (const [scenario, code] of [
it.each([
["missing", 2],
["empty", 2],
["locked", 3],
["cancelled", 3],
["denied", 3],
["unavailable", 4],
["unloaded", 4],
]) {
it(`reports ${scenario} without emitting a secret`, () => {
const result = run([scenario]);
expect(result.status).toBe(code);
expect(result.stdout.length).toBe(0);
});
}
])("reports %s without emitting a secret", (scenario, code) => {
const result = run([scenario]);
expect(result.status).toBe(code);
expect(result.stdout.length).toBe(0);
});
it("rejects invalid arguments before accessing the keyring", () => {
for (const args of [[], [""], ["chrome", "extra"]]) {
const result = run(args);
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/app/DesktopClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,9 @@ it.effect(
},
);

for (const entry of ["startup", "open-url"] as const) {
it.effect(`receives hosted web sign-in through the desktop ${entry} handler`, () =>
it.effect.each(["startup", "open-url"] as const)(
"receives hosted web sign-in through the desktop %s handler",
(entry) =>
Effect.gen(function* () {
storageMock.mockReturnValue(storageAdapter);
createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true });
Expand Down Expand Up @@ -388,5 +389,4 @@ for (const entry of ["startup", "open-url"] as const) {
),
);
}).pipe(Effect.scoped),
);
}
);
9 changes: 5 additions & 4 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ function makeDesktopWindowLayer(
}

describe("DesktopLifecycle", () => {
for (const platform of ["darwin", "win32", "linux"] satisfies ReadonlyArray<NodeJS.Platform>) {
it.effect(`lets the updater's quit event proceed on ${platform}`, () => {
it.effect.each(["darwin", "win32", "linux"] satisfies ReadonlyArray<NodeJS.Platform>)(
"lets the updater's quit event proceed on %s",
(platform) => {
const appListeners = new Map<string, (...args: readonly unknown[]) => void>();
let windowsDestroyed = false;
const environmentLayer = Layer.succeed(DesktopEnvironment.DesktopEnvironment, {
Expand Down Expand Up @@ -153,8 +154,8 @@ describe("DesktopLifecycle", () => {
assert.isTrue(yield* Ref.get(state.quitting));
}),
).pipe(Effect.provide(layer));
});
}
},
);

it.effect("destroys windows before waiting for backend shutdown", () =>
Effect.gen(function* () {
Expand Down
70 changes: 34 additions & 36 deletions apps/desktop/src/app/DesktopPreReadyPlatform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,43 +75,41 @@ describe("DesktopPreReadyPlatform", () => {
);
});

for (const previousEntry of [undefined, 'Exec="/Applications/deleted-previous.AppImage" %U']) {
it.effect(
`prepares a ${previousEntry ? "stale" : "missing"} Linux desktop entry before startup yields`,
() => {
vi.stubEnv("VITE_DEV_SERVER_URL", "");
vi.stubEnv("XDG_DATA_HOME", "/xdg");
vi.stubEnv("APPIMAGE", "/Applications/current.AppImage");
getSwitchValueMock.mockReturnValue("");
let desktopName = "pylon-code.desktop";
let desktopEntry = previousEntry;
setDesktopNameMock.mockImplementation((name: string) => {
desktopName = name;
});
writeFileSyncMock.mockImplementation((path: string, contents: string) => {
if (path === "/xdg/applications/com.pylon.code.desktop") desktopEntry = contents;
});
it.effect.each([
{ previousEntry: undefined, name: "missing" },
{ previousEntry: 'Exec="/Applications/deleted-previous.AppImage" %U', name: "stale" },
])("prepares a $name Linux desktop entry before startup yields", ({ previousEntry }) => {
vi.stubEnv("VITE_DEV_SERVER_URL", "");
vi.stubEnv("XDG_DATA_HOME", "/xdg");
vi.stubEnv("APPIMAGE", "/Applications/current.AppImage");
getSwitchValueMock.mockReturnValue("");
let desktopName = "pylon-code.desktop";
let desktopEntry = previousEntry;
setDesktopNameMock.mockImplementation((name: string) => {
desktopName = name;
});
writeFileSyncMock.mockImplementation((path: string, contents: string) => {
if (path === "/xdg/applications/com.pylon.code.desktop") desktopEntry = contents;
});

return Effect.scoped(
Effect.gen(function* () {
const portalIdentity = Promise.resolve().then(() => ({ desktopName, desktopEntry }));
yield* Layer.build(
DesktopPreReadyPlatform.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
),
);
const identity = yield* Effect.promise(() => portalIdentity);
assert.equal(identity.desktopName, "com.pylon.code.desktop");
assert.include(identity.desktopEntry ?? "", 'Exec="/Applications/current.AppImage" %U');
assert.include(identity.desktopEntry ?? "", "Name=Pylon (Alpha)");
assert.include(identity.desktopEntry ?? "", "MimeType=x-scheme-handler/pylon-code;");
// Same icon path the URL handler installs, so its later check sees an identical entry.
assert.include(identity.desktopEntry ?? "", "Icon=/xdg/com.pylon.code/icon.png");
}),
).pipe(Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())));
},
);
}
return Effect.scoped(
Effect.gen(function* () {
const portalIdentity = Promise.resolve().then(() => ({ desktopName, desktopEntry }));
yield* Layer.build(
DesktopPreReadyPlatform.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
),
);
const identity = yield* Effect.promise(() => portalIdentity);
assert.equal(identity.desktopName, "com.pylon.code.desktop");
assert.include(identity.desktopEntry ?? "", 'Exec="/Applications/current.AppImage" %U');
assert.include(identity.desktopEntry ?? "", "Name=Pylon (Alpha)");
assert.include(identity.desktopEntry ?? "", "MimeType=x-scheme-handler/pylon-code;");
// Same icon path the URL handler installs, so its later check sees an identical entry.
assert.include(identity.desktopEntry ?? "", "Icon=/xdg/com.pylon.code/icon.png");
}),
).pipe(Effect.ensuring(Effect.sync(() => vi.unstubAllEnvs())));
});

it.effect("keeps startup available when the early desktop entry cannot be written", () => {
getSwitchValueMock.mockReturnValue("");
Expand Down
70 changes: 34 additions & 36 deletions apps/desktop/src/app/DesktopUserData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,37 @@ it.effect("identifies a failed source read and preserves its cause", () => {
);
});

for (const sourceName of ["pylon-code", "Pylon (Alpha)"]) {
it.effect(
`preserves Windows credential keys from ${sourceName} without copying browser databases`,
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-v2-profile-" });
const source = path.join(directory, sourceName);
const destination = path.join(directory, "pylon-code-v2");
const state = '{"os_crypt":{"encrypted_key":"test-encrypted-key"}}';
yield* fs.makeDirectory(path.join(directory, "Pylon (Alpha)"), { recursive: true });
yield* fs.makeDirectory(path.join(source, "IndexedDB"), { recursive: true });
yield* fs.writeFileString(path.join(source, "Local State"), state);
yield* fs.writeFileString(path.join(source, "IndexedDB", "LOCK"), "V1 owns this database");
yield* resolveUserDataPath({
appDataDirectory: directory,
isDevelopment: false,
platform: "win32",
});
assert.equal(yield* fs.readFileString(path.join(destination, "Local State")), state);
assert.equal(yield* fs.readFileString(path.join(source, "Local State")), state);
assert.isFalse(yield* fs.exists(path.join(destination, "IndexedDB")));
yield* fs.writeFileString(path.join(destination, "Local State"), "existing V2 state");
yield* resolveUserDataPath({
appDataDirectory: directory,
isDevelopment: false,
platform: "win32",
});
assert.equal(
yield* fs.readFileString(path.join(destination, "Local State")),
"existing V2 state",
);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
}
it.effect.each(["pylon-code", "Pylon (Alpha)"])(
"preserves Windows credential keys from %s without copying browser databases",
(sourceName) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-v2-profile-" });
const source = path.join(directory, sourceName);
const destination = path.join(directory, "pylon-code-v2");
const state = '{"os_crypt":{"encrypted_key":"test-encrypted-key"}}';
yield* fs.makeDirectory(path.join(directory, "Pylon (Alpha)"), { recursive: true });
yield* fs.makeDirectory(path.join(source, "IndexedDB"), { recursive: true });
yield* fs.writeFileString(path.join(source, "Local State"), state);
yield* fs.writeFileString(path.join(source, "IndexedDB", "LOCK"), "V1 owns this database");
yield* resolveUserDataPath({
appDataDirectory: directory,
isDevelopment: false,
platform: "win32",
});
assert.equal(yield* fs.readFileString(path.join(destination, "Local State")), state);
assert.equal(yield* fs.readFileString(path.join(source, "Local State")), state);
assert.isFalse(yield* fs.exists(path.join(destination, "IndexedDB")));
yield* fs.writeFileString(path.join(destination, "Local State"), "existing V2 state");
yield* resolveUserDataPath({
appDataDirectory: directory,
isDevelopment: false,
platform: "win32",
});
assert.equal(
yield* fs.readFileString(path.join(destination, "Local State")),
"existing V2 state",
);
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
48 changes: 23 additions & 25 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,29 @@ describe("ElectronNotification lifetime", () => {
assert.strictEqual(NodeV8.queryObjects(NativeNotification), baseline);
}),
);
for (const event of ["click", "close", "failed"]) {
it.effect(`releases a notification after ${event}`, () =>
Effect.gen(function* () {
const baseline = NodeV8.queryObjects(NativeNotification);
let clicked = 0;
NativeNotification.eventOnShow = event;
try {
yield* Effect.gen(function* () {
const service = yield* ElectronNotification.ElectronNotification;
yield* service.show({
title: "Thread",
body: "Done",
onClick: () => {
clicked++;
},
});
assert.strictEqual(NodeV8.queryObjects(NativeNotification), baseline);
assert.strictEqual(clicked, event === "click" ? 1 : 0);
}).pipe(Effect.provide(ElectronNotification.layer), Effect.scoped);
} finally {
NativeNotification.eventOnShow = undefined;
}
}),
);
}
it.effect.each(["click", "close", "failed"])("releases a notification after %s", (event) =>
Effect.gen(function* () {
const baseline = NodeV8.queryObjects(NativeNotification);
let clicked = 0;
NativeNotification.eventOnShow = event;
try {
yield* Effect.gen(function* () {
const service = yield* ElectronNotification.ElectronNotification;
yield* service.show({
title: "Thread",
body: "Done",
onClick: () => {
clicked++;
},
});
assert.strictEqual(NodeV8.queryObjects(NativeNotification), baseline);
assert.strictEqual(clicked, event === "click" ? 1 : 0);
}).pipe(Effect.provide(ElectronNotification.layer), Effect.scoped);
} finally {
NativeNotification.eventOnShow = undefined;
}
}),
);
it.effect("retains a Windows notification moved into Action Center", () =>
Effect.gen(function* () {
const baseline = NodeV8.queryObjects(NativeNotification);
Expand Down
74 changes: 36 additions & 38 deletions apps/desktop/src/preview/BrowserImport/FirefoxCookies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,45 +386,43 @@ describe("parseFirefoxProfiles", () => {
}),
);

for (const [platform, root] of [
["Linux", "/home/user/.mozilla/firefox"],
["macOS", "/Users/user/Library/Application Support/Firefox"],
] as const) {
it.effect(`validates relative and absolute ${platform} profile paths`, () =>
Effect.gen(function* () {
const parsed = yield* parsePosixFirefoxProfiles(
[
"[Profile0]",
"Name=Relative",
"IsRelative=1",
"Path=Profiles/relative.default",
"[Profile1]",
"Name=Custom",
"IsRelative=0",
"Path=/mnt/custom/firefox-profile",
"[Profile2]",
"IsRelative=1",
"Path=../../escape",
"[Profile3]",
"IsRelative=1",
"Path=/absolute-marked-relative",
"[Profile4]",
"IsRelative=0",
"Path=relative-marked-absolute",
"[Profile5]",
"IsRelative=1",
"Path=Profiles/nul\u0000escape",
].join("\n"),
root,
);
it.effect.each([
{ platform: "Linux", root: "/home/user/.mozilla/firefox" },
{ platform: "macOS", root: "/Users/user/Library/Application Support/Firefox" },
])("validates relative and absolute $platform profile paths", ({ root }) =>
Effect.gen(function* () {
const parsed = yield* parsePosixFirefoxProfiles(
[
"[Profile0]",
"Name=Relative",
"IsRelative=1",
"Path=Profiles/relative.default",
"[Profile1]",
"Name=Custom",
"IsRelative=0",
"Path=/mnt/custom/firefox-profile",
"[Profile2]",
"IsRelative=1",
"Path=../../escape",
"[Profile3]",
"IsRelative=1",
"Path=/absolute-marked-relative",
"[Profile4]",
"IsRelative=0",
"Path=relative-marked-absolute",
"[Profile5]",
"IsRelative=1",
"Path=Profiles/nul\u0000escape",
].join("\n"),
root,
);

expect(parsed).toEqual([
{ directory: "Profiles/relative.default", name: "Relative" },
{ directory: "/mnt/custom/firefox-profile", name: "Custom" },
]);
}),
);
}
expect(parsed).toEqual([
{ directory: "Profiles/relative.default", name: "Relative" },
{ directory: "/mnt/custom/firefox-profile", name: "Custom" },
]);
}),
);

it.effect("uses Windows path rules for relative and absolute profiles", () =>
Effect.gen(function* () {
Expand Down
Loading
Loading