mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb8b153f47 | ||
|
|
3bab9e8c6d | ||
|
|
0c4bf8bdbc | ||
|
|
fa7c1b0672 | ||
|
|
4422d87f95 | ||
|
|
63dad27fe7 | ||
|
|
cb44f17d2a | ||
|
|
075c0dd59b | ||
|
|
bd56c9fe73 | ||
|
|
f7b69003e0 | ||
|
|
5279e3cc43 | ||
|
|
27a656af9e | ||
|
|
c94f791e03 | ||
|
|
334710a4f9 | ||
|
|
e80199f61d | ||
|
|
b2c392558a | ||
|
|
8775649fc3 | ||
|
|
cec7fe746c | ||
|
|
4fe498edd5 | ||
|
|
67b2e8074e | ||
|
|
01e4ff9c12 |
No files matched your search
@@ -176,10 +176,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Missing when the PR leg failed; `coverage (pr)` already reports that.
|
||||
- name: Download PR coverage
|
||||
id: pr-coverage
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: puterjs-coverage-pr
|
||||
@@ -189,7 +186,6 @@ jobs:
|
||||
# base run) — then report without the comparison column.
|
||||
- name: Download base coverage
|
||||
id: base-coverage
|
||||
if: steps.pr-coverage.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
@@ -197,7 +193,7 @@ jobs:
|
||||
path: coverage-base
|
||||
|
||||
- name: Report coverage on PR
|
||||
if: steps.pr-coverage.outcome == 'success' && steps.base-coverage.outcome == 'success'
|
||||
if: steps.base-coverage.outcome == 'success'
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
with:
|
||||
name: puter.js SDK
|
||||
@@ -206,7 +202,7 @@ jobs:
|
||||
json-summary-compare-path: coverage-base/coverage-summary.json
|
||||
|
||||
- name: Report coverage on PR (no base comparison)
|
||||
if: steps.pr-coverage.outcome == 'success' && steps.base-coverage.outcome != 'success'
|
||||
if: steps.base-coverage.outcome != 'success'
|
||||
uses: davelosert/vitest-coverage-report-action@v2
|
||||
with:
|
||||
name: puter.js SDK
|
||||
|
||||
@@ -144,22 +144,11 @@ const createInline = async (
|
||||
return String(response.body.subId);
|
||||
};
|
||||
|
||||
/**
|
||||
* The post each label made, in order, once all have arrived. Matched by label
|
||||
* since earlier tests' subscriptions stay live and post to the same sink.
|
||||
*/
|
||||
const delivered = async (...labels: string[]): Promise<SinkPost[]> => {
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(sinkPosts.map((post) => post.label)).toEqual(
|
||||
expect.arrayContaining(labels),
|
||||
),
|
||||
{ timeout: 30_000, interval: 50 },
|
||||
);
|
||||
return labels.map((label) =>
|
||||
sinkPosts.find((post) => post.label === label)!,
|
||||
);
|
||||
};
|
||||
const delivered = (count = 1): Promise<void> =>
|
||||
vi.waitFor(() => expect(sinkPosts.length).toBeGreaterThanOrEqual(count), {
|
||||
timeout: 30_000,
|
||||
interval: 50,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
env = await setupPuterTestEnv({
|
||||
@@ -266,11 +255,14 @@ describe('an inline handler in a user scope, end to end', () => {
|
||||
await env.server.services.fs.touch(holder.id, {
|
||||
path: `${holder.anchor}/note.txt`,
|
||||
});
|
||||
const [post] = await delivered('inline');
|
||||
await delivered();
|
||||
|
||||
expect(post.path).toBe(`${holder.anchor}/note.txt`);
|
||||
expect(sinkPosts[0]).toMatchObject({
|
||||
label: 'inline',
|
||||
path: `${holder.anchor}/note.txt`,
|
||||
});
|
||||
const { actor } = await env.server.services.auth.authenticate(
|
||||
post.token!,
|
||||
sinkPosts[0].token!,
|
||||
);
|
||||
expect(actor!.user?.id).toBe(holder.id);
|
||||
expect(actor!.effectiveApp?.uid).toBe(appUid);
|
||||
@@ -305,14 +297,14 @@ describe('an inline handler in a user scope, end to end', () => {
|
||||
await env.server.services.fs.touch(holder.id, {
|
||||
path: `${holder.anchor}/shared-${i}.txt`,
|
||||
});
|
||||
// The first test's 'inline' handler also posts for holders[0].
|
||||
const posts = await delivered('first', 'second');
|
||||
for (const [i, post] of posts.entries()) {
|
||||
const { actor } = await env.server.services.auth.authenticate(
|
||||
post.token!,
|
||||
await delivered(2);
|
||||
const runAs = new Set<number>();
|
||||
for (const post of sinkPosts)
|
||||
runAs.add(
|
||||
(await env.server.services.auth.authenticate(post.token!))
|
||||
.actor!.user!.id,
|
||||
);
|
||||
expect(actor!.user!.id).toBe(holders[i].id);
|
||||
}
|
||||
expect(runAs).toEqual(new Set(holders.map((h) => h.id)));
|
||||
create.mockRestore();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -78,10 +78,7 @@ describe('LocalWorkerService shutdown disposal', () => {
|
||||
disposals[0].mockImplementationOnce(async () => {
|
||||
await held;
|
||||
});
|
||||
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
||||
// The test server keeps arming timers from real callbacks while fakes
|
||||
// are installed, so only what shutdown arms synchronously is checked.
|
||||
const arm = vi.spyOn(globalThis, 'setTimeout');
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
let finished = false;
|
||||
const draining = Promise.resolve(service.onServerShutdown()).then(
|
||||
@@ -89,13 +86,12 @@ describe('LocalWorkerService shutdown disposal', () => {
|
||||
finished = true;
|
||||
},
|
||||
);
|
||||
expect(arm.mock.calls.map(([, ms]) => ms)).toEqual([5_000]);
|
||||
await vi.advanceTimersByTimeAsync(4_999);
|
||||
expect(finished).toBe(false);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await draining;
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
} finally {
|
||||
arm.mockRestore();
|
||||
release();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -2860,12 +2860,8 @@ describe('OpenAIRealtimeRelayService relay', () => {
|
||||
),
|
||||
).toHaveLength(64),
|
||||
);
|
||||
// Holds are reserved and usage recorded one at a time, which a loaded
|
||||
// runner takes past vi.waitFor's default second.
|
||||
const settled = (check: () => Promise<void> | void) =>
|
||||
vi.waitFor(check, { timeout: 5_000 });
|
||||
// The pool grew one item per commit, with nothing spare at the cap.
|
||||
await settled(async () =>
|
||||
await vi.waitFor(async () =>
|
||||
expect(
|
||||
await server.services.metering.getOutstandingHolds(actor),
|
||||
).toBe(64 * 10_896_000),
|
||||
@@ -2894,7 +2890,7 @@ describe('OpenAIRealtimeRelayService relay', () => {
|
||||
for (let batch = 0; batch < 8; batch++) {
|
||||
for (let i = Math.max(1, batch * 8); i < (batch + 1) * 8; i++)
|
||||
complete(i);
|
||||
await settled(() =>
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
recorded.mock.calls
|
||||
.flatMap(([, rows]) => rows)
|
||||
@@ -2910,13 +2906,13 @@ describe('OpenAIRealtimeRelayService relay', () => {
|
||||
.filter((row) => row.usageType.includes('transcription_'))
|
||||
.reduce((sum, row) => sum + row.costOverride!, 0),
|
||||
).toBe(640);
|
||||
await settled(async () =>
|
||||
await vi.waitFor(async () =>
|
||||
expect(
|
||||
await server.services.metering.getOutstandingHolds(actor),
|
||||
).toBe(2 * 10_896_000),
|
||||
);
|
||||
c.ws.close();
|
||||
await settled(async () =>
|
||||
await vi.waitFor(async () =>
|
||||
expect(
|
||||
await server.services.metering.getOutstandingHolds(actor),
|
||||
).toBe(0),
|
||||
|
||||
@@ -1542,22 +1542,18 @@ describe('AppStore detailed stats', () => {
|
||||
let db;
|
||||
|
||||
const APP_UID = `app-stats-${Date.now()}`;
|
||||
// The windows come from the clock, so it is pinned: mid-week and
|
||||
// mid-month, and minutes clear of midnight in any time zone.
|
||||
const NOW = new Date('2026-06-17T12:07:30Z');
|
||||
const nowSeconds = Math.floor(NOW.getTime() / 1000);
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
|
||||
beforeAll(async () => {
|
||||
server = await setupTestServer();
|
||||
appStore = server.stores.app;
|
||||
db = server.clients.db;
|
||||
await clearAppCache(server.clients.redis);
|
||||
vi.useFakeTimers({ toFake: ['Date'], now: NOW });
|
||||
|
||||
// Two users in one hour. Windows end at now, exclusive.
|
||||
// Two users; the repeat shares a timestamp so it is always same-hour.
|
||||
for (const [userId, offset] of [
|
||||
[1, 60],
|
||||
[1, 60],
|
||||
[1, 0],
|
||||
[1, 0],
|
||||
[2, 120],
|
||||
]) {
|
||||
await db.write(
|
||||
@@ -1568,7 +1564,6 @@ describe('AppStore detailed stats', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
vi.useRealTimers();
|
||||
await server?.shutdown();
|
||||
});
|
||||
|
||||
|
||||
@@ -154,9 +154,6 @@ export const setupTestServer = async (
|
||||
redis: { useMock: true },
|
||||
// Suites run system jobs themselves, not on the poll timer.
|
||||
jobs: { enabled: false },
|
||||
// The health loop's timed `SELECT 1` would land in tests that
|
||||
// count queries.
|
||||
server_health: { disabled_checks: ['database-liveness'] },
|
||||
s3: { localConfig: { inMemory: true } },
|
||||
no_default_user: true,
|
||||
no_devwatch: true,
|
||||
|
||||
@@ -37,9 +37,6 @@ const repoRoot = path.resolve(backendDir, '../..');
|
||||
const isPgmockMode =
|
||||
(process.env.PUTER_TEST_DB_ENGINE ?? '').toLowerCase() === 'postgres';
|
||||
const pgmockTimeoutMs = 600_000;
|
||||
// Loaded CI runners stall timers well past the 5 s / 10 s defaults; a real
|
||||
// hang still fails, just later.
|
||||
const ciTimeouts = { testTimeout: 20_000, hookTimeout: 60_000 };
|
||||
|
||||
// Tests that always boot pgmock regardless of PUTER_TEST_DB_ENGINE. Too slow
|
||||
// for CI; run locally or via `npm run test:backend:postgres`.
|
||||
@@ -106,11 +103,9 @@ export const lowerDecoratorsPlugin = {
|
||||
loader: 'ts',
|
||||
target: 'es2024',
|
||||
sourcefile: id,
|
||||
sourcemap: 'external',
|
||||
sourcemap: 'inline',
|
||||
});
|
||||
// Without the map, coverage and stack traces report esbuild's
|
||||
// output lines instead of the source's.
|
||||
return { code: result.code, map: result.map };
|
||||
return { code: result.code, map: null };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -150,9 +145,7 @@ export default defineConfig(({ mode }) => ({
|
||||
hookTimeout: pgmockTimeoutMs,
|
||||
fileParallelism: false,
|
||||
}
|
||||
: isCi
|
||||
? ciTimeouts
|
||||
: {}),
|
||||
: {}),
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: isCi
|
||||
|
||||
@@ -194,6 +194,7 @@ export function pushSignupScreen (flow, options = {}) {
|
||||
// them, and an identity provider's refusal shows here too.
|
||||
h += '<div class="dashboard-form-status auth-screen-status" role="status" aria-live="polite"></div>';
|
||||
h += '<div class="signup-bonus-notice" hidden></div>';
|
||||
h += `<p class="signup-oidc-only-notice" hidden>${i18n('signup_oidc_only')}</p>`;
|
||||
h += '<div class="auth-providers oidc-providers-wrapper" hidden>';
|
||||
h += '<div class="auth-provider-list"></div>';
|
||||
h += '</div>';
|
||||
@@ -268,6 +269,7 @@ export function pushSignupScreen (flow, options = {}) {
|
||||
$form.prop('hidden', true);
|
||||
$screen.find('.signup-with-email-wrapper').prop('hidden', true);
|
||||
$screen.find('.oidc-providers-wrapper').prop('hidden', false);
|
||||
$screen.find('.signup-oidc-only-notice').prop('hidden', false);
|
||||
};
|
||||
|
||||
// Turnstile renders into the form once its script has loaded.
|
||||
|
||||
@@ -71,13 +71,15 @@ describe('signup when only OIDC signups are open', () => {
|
||||
await open();
|
||||
expect($('.signup-with-email-wrapper').prop('hidden')).toBe(false);
|
||||
expect($('.signup-form').prop('hidden')).toBe(true);
|
||||
expect($('.signup-oidc-only-notice').prop('hidden')).toBe(true);
|
||||
$('.signup-with-email-btn').trigger('click');
|
||||
expect($('.signup-form').prop('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
it('drops the email form', async () => {
|
||||
it('drops the email form and says why', async () => {
|
||||
providers({ providers: ['google'], signupOidcOnly: true });
|
||||
await open();
|
||||
expect($('.signup-oidc-only-notice').prop('hidden')).toBe(false);
|
||||
expect($('.signup-form').prop('hidden')).toBe(true);
|
||||
expect($('.signup-with-email-wrapper').prop('hidden')).toBe(true);
|
||||
});
|
||||
@@ -92,6 +94,7 @@ describe('signup when only OIDC signups are open', () => {
|
||||
await vi.waitFor(() => expect(calls).toHaveLength(1));
|
||||
calls[0].error({ responseText: JSON.stringify({ code: 'signup_oidc_only', message: 'closed' }) });
|
||||
await tick();
|
||||
expect($('.signup-oidc-only-notice').prop('hidden')).toBe(false);
|
||||
expect($('.signup-form').prop('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -201,9 +201,6 @@ export default function openAuthFlow ({ page = false, dismissable = ! page, clos
|
||||
|
||||
const { $overlay, $dialog, close: closeOverlay, isClosed } = mountDashboardOverlay({
|
||||
html: h,
|
||||
// A page already fills the screen. Resizing it under iOS's own pan to
|
||||
// the focused field makes the form jump up and down as the keyboard opens.
|
||||
fitKeyboard: ! as_page,
|
||||
canDismiss: (via) => dismissable
|
||||
&& (via === 'close' || (! busy && (via !== 'backdrop' || closeOnBackdrop))),
|
||||
onDismiss: () => {
|
||||
|
||||
@@ -72,11 +72,9 @@ export function bindBackdropDismiss ($overlay, isBackdrop, dismiss) {
|
||||
* overlay, after it has closed. Not called for `close()`.
|
||||
* @param {(via: 'escape'|'close'|'backdrop') => boolean} [opts.canDismiss]
|
||||
* Consulted before each dismissal; returning false keeps the overlay up.
|
||||
* @param {boolean} [opts.fitKeyboard] Shrink to the part of the screen an
|
||||
* on-screen keyboard leaves visible. Defaults to true.
|
||||
* @returns {{ $overlay: jQuery, $dialog: jQuery, close: () => void, isClosed: () => boolean }}
|
||||
*/
|
||||
export default function mountDashboardOverlay ({ html, $container, returnFocusTo, onDismiss, canDismiss, fitKeyboard = true }) {
|
||||
export default function mountDashboardOverlay ({ html, $container, returnFocusTo, onDismiss, canDismiss }) {
|
||||
const $root = $container && $container.length ? $container : $('body');
|
||||
const ns = `.dashboard-overlay-${++overlay_seq}`;
|
||||
const el_previous_focus = document.activeElement;
|
||||
@@ -89,7 +87,7 @@ export default function mountDashboardOverlay ({ html, $container, returnFocusTo
|
||||
// iOS lays its keyboard over the page rather than resizing it, which
|
||||
// would leave a bottom sheet's fields and buttons underneath. Pin the
|
||||
// overlay to the part of the screen that is still visible.
|
||||
const viewport = fitKeyboard ? window.visualViewport : null;
|
||||
const viewport = window.visualViewport;
|
||||
const fitToViewport = () => {
|
||||
const covered = window.innerHeight - viewport.height - viewport.offsetTop;
|
||||
const pinned = covered > 1 || viewport.offsetTop > 1;
|
||||
|
||||
@@ -150,13 +150,6 @@ describe('mountDashboardOverlay', () => {
|
||||
close();
|
||||
expect(window.visualViewport.removeEventListener).toHaveBeenCalledWith('resize', listeners.resize);
|
||||
});
|
||||
|
||||
it('leaves its size alone under a keyboard when told not to fit it', () => {
|
||||
window.visualViewport = { height: window.innerHeight, offsetTop: 0, addEventListener: vi.fn(), removeEventListener: vi.fn() };
|
||||
const { close } = mountDashboardOverlay({ html: card, fitKeyboard: false });
|
||||
expect(window.visualViewport.addEventListener).not.toHaveBeenCalled();
|
||||
close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bindBackdropDismiss', () => {
|
||||
|
||||
@@ -10692,11 +10692,9 @@ body.dashboard-mode .notifications-close-all {
|
||||
z-index: 99999998;
|
||||
}
|
||||
|
||||
/* As a page there is nothing behind the card to go back to. It covers the
|
||||
screen at once rather than fading in over whatever was there. */
|
||||
/* As a page there is nothing behind the card to go back to. */
|
||||
.dashboard-dialog-overlay.auth-dialog-page {
|
||||
background: var(--dashboard-sidebar-background);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.dashboard-dialog.auth-dialog {
|
||||
@@ -11073,6 +11071,13 @@ body.dashboard-mode .notifications-close-all {
|
||||
color: var(--dashboard-danger-text);
|
||||
}
|
||||
|
||||
.auth-dialog .signup-oidc-only-notice {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--dashboard-text-secondary);
|
||||
}
|
||||
|
||||
.auth-dialog .oidc-providers-wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -1925,60 +1925,6 @@ span.header-sort-icon img {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Dark mode: same surfaces as the dashboard's dark cards. The translucent
|
||||
background is also set in the backdrop-filter @supports block below. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.context-menu {
|
||||
color: #d4d4d4;
|
||||
background-color: rgb(38 38 38 / 85%);
|
||||
border-color: #3d3d3d;
|
||||
box-shadow: 0px 3px 20px #00000066;
|
||||
}
|
||||
|
||||
.context-menu .context-menu-divider>hr {
|
||||
border-top-color: #ffffff26;
|
||||
}
|
||||
|
||||
.context-menu .has-open-context-menu-submenu {
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
/* `color: initial` resolves to black, unreadable on the dark surface. */
|
||||
.context-menu .context-menu-item-active-blurred {
|
||||
background-color: #404650;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.context-menu .context-menu-item-disabled, .context-menu .context-menu-item-disabled:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* The resting chevron is black; the active one is already white. */
|
||||
.context-menu .submenu-arrow:not(.submenu-arrow-active) {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* Full-color icons (file types, app icons) keep their colors; only
|
||||
monochrome dark glyphs are inverted. */
|
||||
.context-menu .context-menu-item .ctx-item-icon-monochrome {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.context-menu.context-menu-sheet {
|
||||
background-color: rgba(38, 38, 38, 0.96) !important;
|
||||
box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.context-menu.context-menu-sheet .context-menu-item-active:not(.context-menu-divider) {
|
||||
background-color: rgba(10, 132, 255, 0.24) !important;
|
||||
color: #d4d4d4 !important;
|
||||
}
|
||||
|
||||
.context-menu.context-menu-sheet .context-menu-divider > hr {
|
||||
border-top-color: rgba(235, 235, 245, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-count-badge {
|
||||
background-color: red;
|
||||
border: 2px solid white;
|
||||
@@ -2314,6 +2260,13 @@ label {
|
||||
color: #611a15;
|
||||
}
|
||||
|
||||
.signup-oidc-only-notice {
|
||||
text-align: center;
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
/* -- Logo strip above the login / signup forms -- */
|
||||
.auth-logo-header {
|
||||
display: flex;
|
||||
@@ -3979,12 +3932,6 @@ fieldset[name=number-code] {
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.context-menu {
|
||||
background-color: rgb(38 38 38 / 92%);
|
||||
}
|
||||
}
|
||||
|
||||
.popover:not(.device-phone .popover) {
|
||||
background-color: rgb(238, 243, 248);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
@@ -996,6 +996,7 @@ const en = {
|
||||
sign_in_with_provider: 'Sign in with %%',
|
||||
sign_up_with_provider: 'Sign up with %%',
|
||||
sign_up_with_email: 'Sign up using email',
|
||||
signup_oidc_only: 'New accounts can only be created with one of the options below.',
|
||||
popup_opener_uses_puter: '%strong% is powered by Puter for its AI and cloud features.\nUse your Puter account to continue.',
|
||||
popup_opener_origin_unsupported: 'Puter cannot sign you in to this page because the browser gives it no identifiable address. Pages opened straight from a file (file://) and iframes sandboxed without allow-same-origin are not supported — serve the page over http://localhost or a real domain and try again.',
|
||||
oidc_switched_to_login_message: 'You have been logged in to an existing account.',
|
||||
|
||||
+14
-20
@@ -1874,13 +1874,9 @@ window.initgui = async function (options) {
|
||||
|
||||
//--------------------------------------------------------------------------------------
|
||||
// Desktop background (early)
|
||||
// Set before action=login/signup so the signed-in dialog has the desktop behind it.
|
||||
// Skipped for the full-page form, which it would only flash behind before covering.
|
||||
// Set before action=login/signup so OIDC error redirects show the background behind the form.
|
||||
// -------------------------------------------------------------------------------------
|
||||
const authActionIsPage =
|
||||
action === 'password-recovery' ||
|
||||
(!window.is_auth() && (action === 'login' || action === 'signup'));
|
||||
if (!window.is_fullpage_mode && !window.embedded_in_popup && !authActionIsPage) {
|
||||
if (!window.is_fullpage_mode && !window.embedded_in_popup) {
|
||||
window.refresh_desktop_background();
|
||||
}
|
||||
|
||||
@@ -2430,23 +2426,12 @@ window.initgui = async function (options) {
|
||||
window.update_sites_cache();
|
||||
}
|
||||
}
|
||||
// App landing pages (`/app/<name>`, incl. `/desktop/app/<name>`) require a
|
||||
// real account even on a first visit — never a temp user. So does a share
|
||||
// link: a share only ever reaches a real account, so a temporary one could
|
||||
// never see what it points at.
|
||||
const is_app_landing_page = window.url_paths[0] === 'app' && !!window.url_paths[1];
|
||||
const is_share_link = window.url_query_params.has('shared');
|
||||
const needsSignIn =
|
||||
!window.is_auth() &&
|
||||
(!window.first_visit_ever || window.disable_temp_users || is_app_landing_page || is_share_link);
|
||||
|
||||
// -------------------------------------------------------------------------------------
|
||||
// Desktop Background
|
||||
// If we're in fullpage/emebedded/Auth Popup mode, we don't want to load the custom background
|
||||
// because it's not visible anyway and it's a waste of bandwidth. Same behind the sign-in page.
|
||||
// because it's not visible anyway and it's a waste of bandwidth
|
||||
// -------------------------------------------------------------------------------------
|
||||
const showsSignInPage = needsSignIn && window.logged_in_users.length === 0;
|
||||
if (!window.is_fullpage_mode && !window.embedded_in_popup && !showsSignInPage) {
|
||||
if (!window.is_fullpage_mode && !window.embedded_in_popup) {
|
||||
window.refresh_desktop_background();
|
||||
}
|
||||
// -------------------------------------------------------------------------------------
|
||||
@@ -2457,7 +2442,16 @@ window.initgui = async function (options) {
|
||||
// then failed to authenticate.
|
||||
if (!window.is_auth()) reveal_anonymous_markup(true);
|
||||
|
||||
if (needsSignIn) {
|
||||
// App landing pages (`/app/<name>`, incl. `/desktop/app/<name>`) require a
|
||||
// real account even on a first visit — never a temp user. So does a share
|
||||
// link: a share only ever reaches a real account, so a temporary one could
|
||||
// never see what it points at.
|
||||
const is_app_landing_page = window.url_paths[0] === 'app' && !!window.url_paths[1];
|
||||
const is_share_link = window.url_query_params.has('shared');
|
||||
if (
|
||||
!window.is_auth() &&
|
||||
(!window.first_visit_ever || window.disable_temp_users || is_app_landing_page || is_share_link)
|
||||
) {
|
||||
// `npm start --server=<remote>` serves this GUI locally while pointing
|
||||
// `gui_origin` at a remote Puter. There is nothing here to log into:
|
||||
// `/login` accepts only its own origin, because it answers with a full
|
||||
|
||||
@@ -29,14 +29,15 @@ const resolveBundle = (): string => {
|
||||
);
|
||||
};
|
||||
|
||||
// In coverage mode every vm context is handed the first context's
|
||||
// `__coverage__`: istanbul reuses a file entry whose hash matches, so all
|
||||
// contexts count into one object instead of each holding a ~6 MB copy.
|
||||
let nodeCoverage: IstanbulCoverage | undefined;
|
||||
// In coverage mode every vm context is retained so its `__coverage__`
|
||||
// counters can be merged after the run (each test loads a fresh SDK).
|
||||
const coverageContexts: Array<Record<string, unknown>> = [];
|
||||
|
||||
/** Coverage counters from every SDK context created so far. */
|
||||
export const collectNodeCoverage = (): IstanbulCoverage | undefined =>
|
||||
nodeCoverage;
|
||||
export const collectNodeCoverage = (): Array<IstanbulCoverage | undefined> =>
|
||||
coverageContexts.map(
|
||||
(context) => context.__coverage__ as IstanbulCoverage | undefined,
|
||||
);
|
||||
|
||||
// Compile the ~MB bundle once; isolation comes from a fresh context per
|
||||
// call, not a fresh parse. The filename ties stack traces and V8 coverage
|
||||
@@ -75,12 +76,9 @@ export const loadNodePuter = (env: EnvManifest, token: string): PuterSDK => {
|
||||
// The SDK's nodejs branch only shims localStorage when it's absent;
|
||||
// clear any inherited one so instances never share auth state.
|
||||
delete context.localStorage;
|
||||
if (nodeCoverage) context.__coverage__ = nodeCoverage;
|
||||
|
||||
sdkScript().runInContext(vm.createContext(context));
|
||||
if (coverageEnabled()) {
|
||||
nodeCoverage ??= context.__coverage__ as IstanbulCoverage | undefined;
|
||||
}
|
||||
if (coverageEnabled()) coverageContexts.push(context);
|
||||
|
||||
const puter = context.puter as PuterSDK;
|
||||
puter.setAuthToken(token);
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('puter.js API suites (node)', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
if (coverageEnabled()) {
|
||||
await writeCoverageShard('node', [collectNodeCoverage()]);
|
||||
await writeCoverageShard('node', collectNodeCoverage());
|
||||
}
|
||||
await env?.shutdown();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createCoverageMap } from 'istanbul-lib-coverage';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
setupPuterTestEnv,
|
||||
@@ -7,7 +6,11 @@ import {
|
||||
import type { IConfig } from '@heyputer/backend/types.ts';
|
||||
import { bundleHarnessEntry } from '../harness/bundleHarnessEntry.ts';
|
||||
import { loadPuterJsTestOptions } from '../harness/capabilities.ts';
|
||||
import { coverageEnabled, writeCoverageShard } from '../harness/coverage.ts';
|
||||
import {
|
||||
coverageEnabled,
|
||||
writeCoverageShard,
|
||||
type IstanbulCoverage,
|
||||
} from '../harness/coverage.ts';
|
||||
import { listTests, skipReason, type RunTestArgs } from '../harness/executor.ts';
|
||||
import { loadNodePuter } from '../harness/nodeSdkLoader.ts';
|
||||
import type { EnvManifest, RunTestResult } from '../harness/types.ts';
|
||||
@@ -17,9 +20,8 @@ const WORKER_NAME = 'puterjs-suites';
|
||||
const options = loadPuterJsTestOptions();
|
||||
|
||||
// Counters ride along on every /run response (the workerd isolate can
|
||||
// recycle mid-run). Merged as they arrive, since keeping each ~6 MB
|
||||
// snapshot until the end runs the fork out of heap.
|
||||
const workerdCoverage = createCoverageMap({});
|
||||
// recycle mid-run); merged into one shard after the run.
|
||||
const workerdCoverage: IstanbulCoverage[] = [];
|
||||
|
||||
// The shared puter.js suites running inside local workerd. The suite
|
||||
// bundle is deployed through the real workers pipeline (SDK
|
||||
@@ -59,7 +61,7 @@ describe('puter.js API suites (workerd)', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
if (coverageEnabled()) {
|
||||
await writeCoverageShard('workerd', [workerdCoverage.toJSON()]);
|
||||
await writeCoverageShard('workerd', workerdCoverage);
|
||||
}
|
||||
await env?.shutdown();
|
||||
});
|
||||
@@ -87,7 +89,7 @@ describe('puter.js API suites (workerd)', () => {
|
||||
}
|
||||
const result = JSON.parse(text) as RunTestResult;
|
||||
if (result.coverage) {
|
||||
workerdCoverage.merge(result.coverage);
|
||||
workerdCoverage.push(result.coverage);
|
||||
}
|
||||
expect(result.error ?? '').toBe('');
|
||||
expect(result.ok).toBe(true);
|
||||
|
||||
Reference in new issue
Block a user