Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/desktop/e2e/settings-pricing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ensureSidebarExpanded, expect, test } from './fixtures';

// Real path: 设置 → 使用统计 → 定价配置. The tab reads ONE Host-backed effective
// pricing snapshot (the bundled built-in table ∪ any user overrides) from the
// real embedded Runtime Host — no bridge stub. It exercises #2015 acceptance #1
// (a Host-backed effective table whose built-in rows are labelled by 来源), #2
// (the Pricing tab is not time-scoped: the Usage date range/summary toolbar is
// gone, so the range cannot be mistaken for a Pricing scope), and #11 (the
// price editor returns focus to the trigger that opened it — real Electron
// focus, which the linkedom unit harness cannot honestly exercise).
test('pricing tab shows the Host-backed effective table, is not time-scoped, and restores editor focus', async ({
window: page,
}) => {
await ensureSidebarExpanded(page);
await page.getByRole('button', { name: '设置' }).click();
await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();

await page.getByRole('button', { name: '使用统计', exact: true }).click();
await page
.getByRole('navigation', { name: '使用统计视图' })
.getByRole('button', { name: '定价配置', exact: true })
.click();

// The Pricing panel owns its own explanatory copy and its own Add control,
// instead of the Usage range chrome.
await expect(page.getByText('美元 / 每百万 token。', { exact: false })).toBeVisible();
const addButton = page.getByRole('button', { name: '添加定价' });
await expect(addButton).toBeVisible();

// #2015 acceptance #2: the Usage range + summary toolbar must be absent on the
// Pricing tab so the Usage date range cannot read as a Pricing scope.
await expect(page.getByRole('group', { name: '使用统计范围与刷新' })).toHaveCount(0);
await expect(page.getByRole('group', { name: '使用统计汇总指标' })).toHaveCount(0);

// #2015 acceptance #1: the effective snapshot loads Host-backed rows, and a
// bundled row is distinguished by its 来源 = 内置 label. Scoped to the table
// Card so the empty-state copy (which also contains 内置) cannot satisfy it.
const pricingTable = page.locator('.settingsUsageTable');
await expect(pricingTable).toBeVisible();
await expect(pricingTable.getByText('内置', { exact: true }).first()).toBeVisible();

// #2015 acceptance #11: opening the editor and closing it returns focus to the
// trigger that opened it.
await addButton.click();
const editor = page.getByRole('dialog');
await expect(editor).toBeVisible();
await editor.getByRole('button', { name: '取消' }).click();
await expect(editor).toHaveCount(0);
await expect(addButton).toBeFocused();
});
116 changes: 116 additions & 0 deletions apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"src/renderer/locales/settings-memory-copy.ts",
"src/renderer/locales/settings-navigation-copy.ts",
"src/renderer/locales/settings-preferences-copy.ts",
"src/renderer/locales/settings-pricing-copy.ts",
"src/renderer/locales/settings-projects-copy.ts",
"src/renderer/locales/settings-provider-copy.ts",
"src/renderer/locales/settings-shared-copy.ts",
Expand Down Expand Up @@ -156,6 +157,8 @@
"src/renderer/settings/password-input.tsx",
"src/renderer/settings/permission-center-page.tsx",
"src/renderer/settings/personalization-settings-section.tsx",
"src/renderer/settings/pricing-settings-page.tsx",
"src/renderer/settings/pricing-settings-view-model.ts",
"src/renderer/settings/projects-settings-page.tsx",
"src/renderer/settings/provider-add-form.tsx",
"src/renderer/settings/provider-add-model-dialog.tsx",
Expand Down Expand Up @@ -205,11 +208,13 @@
"src/renderer/settings/tasks-settings-page.tsx",
"src/renderer/settings/ui-locale-update-gate.ts",
"src/renderer/settings/usage-settings-page.tsx",
"src/renderer/settings/usage-stats-table.tsx",
"src/renderer/settings/use-action-guard.ts",
"src/renderer/settings/use-connection-detail.ts",
"src/renderer/settings/use-memory-settings-controller.ts",
"src/renderer/settings/use-oauth-login-flow.ts",
"src/renderer/settings/use-optimistic-settings-draft.ts",
"src/renderer/settings/use-pricing-settings-controller.ts",
"src/renderer/settings/web-search-settings-page.tsx",
"src/renderer/settled-message-merge.ts",
"src/renderer/settled-session-transients.ts",
Expand Down Expand Up @@ -1956,6 +1961,17 @@
"@maka/core/ui-locale": 1
}
},
"src/renderer/locales/settings-pricing-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"@maka/core/ui-locale": 1
}
},
"src/renderer/locales/settings-projects-copy.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down Expand Up @@ -3388,6 +3404,45 @@
"react": 1
}
},
"src/renderer/settings/pricing-settings-page.tsx": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {
"useEffect": 1,
"usePricingSettingsController": 1,
"useState": 1
},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"../locales/settings-pricing-copy.js": 1,
"./pricing-settings-view-model.js": 1,
"./usage-stats-table.js": 1,
"./use-pricing-settings-controller.js": 1,
"@astryxdesign/core": 1,
"@astryxdesign/core/AlertDialog": 1,
"@astryxdesign/core/Collapsible": 1,
"@astryxdesign/core/Dialog": 1,
"@astryxdesign/core/Layout": 1,
"@maka/ui": 1,
"@maka/ui/icons": 1,
"react": 1
}
},
"src/renderer/settings/pricing-settings-view-model.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"@maka/core/usage-stats/pricing": 1,
"@maka/core/usage-stats/types": 1,
"@maka/runtime-host/protocol": 1
}
},
"src/renderer/settings/projects-settings-page.tsx": {
"bridgePaths": {
"window.maka.app.info": 1,
Expand Down Expand Up @@ -4441,9 +4496,11 @@
"dependencyPaths": {
"../../shared/runtime-host-identity.js": 1,
"../locales/settings-usage-copy": 1,
"./pricing-settings-page": 1,
"./settings-error-copy": 1,
"./settings-metric-card": 1,
"./settings-section": 1,
"./usage-stats-table": 1,
"./use-action-guard": 1,
"./use-optimistic-settings-draft": 1,
"@astryxdesign/core": 1,
Expand All @@ -4455,6 +4512,19 @@
"react": 1
}
},
"src/renderer/settings/usage-stats-table.tsx": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"@astryxdesign/core": 1,
"@maka/ui/icons": 1,
"react": 1
}
},
"src/renderer/settings/use-action-guard.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down Expand Up @@ -4602,6 +4672,40 @@
"react": 2
}
},
"src/renderer/settings/use-pricing-settings-controller.ts": {
"bridgePaths": {
"window.maka.settings.pricing.load": 1,
"window.maka.settings.pricing.mutate": 2
},
"environmentCapabilities": {
"requestAnimationFrame": 1
},
"hookCalls": {
"useEffect": 2,
"useKeyedActionGuard": 1,
"useRef": 5,
"useRuntimeHostSettingsErrorReporter": 1,
"useRuntimeHostSettingsGenerationKey": 1,
"useRuntimeHostSettingsTarget": 1,
"useState": 10,
"useToast": 1,
"useUiLocale": 1
},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"../../shared/desktop-pricing.js": 1,
"../locales/settings-pricing-copy.js": 1,
"./pricing-settings-view-model.js": 1,
"./runtime-host-settings-target.js": 1,
"./settings-error-copy.js": 1,
"./use-action-guard.js": 1,
"@maka/runtime-host/protocol": 1,
"@maka/ui": 1,
"react": 1
}
},
"src/renderer/settings/web-search-settings-page.tsx": {
"bridgePaths": {
"window.maka.webSearch.query": 1,
Expand Down Expand Up @@ -5433,6 +5537,18 @@
"@maka/core/llm-connections": 1
}
},
"src/shared/desktop-pricing.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
"hookCalls": {},
"lifecycleMethods": {},
"unresolvedDependencies": 0,
"actionFactories": [],
"dependencyPaths": {
"@maka/core/usage-stats/pricing": 1,
"@maka/runtime-host/protocol": 1
}
},
"src/shared/desktop-session-projection.ts": {
"bridgePaths": {},
"environmentCapabilities": {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ test('projects only present Usage Session ids into the Desktop host namespace',
byProvider: [],
byModel: [],
byTool: [],
pricing: [],
provenance: EMPTY_USAGE_PROVENANCE,
};

Expand Down
92 changes: 92 additions & 0 deletions apps/desktop/src/main/__tests__/pricing-settings-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';

// #2015 acceptance #12: the renderer Pricing surface may reach the Host ONLY
// through the two `window.maka.settings.pricing` IPC capabilities. It must not
// open a Host-owned Store, an embedded Runtime owner, or the main-process
// Runtime Host client, and it must not restore the retired `window.maka.usage`
// bridge or call the three legacy Pricing Main handlers. This is a static gate:
// a future edit that reaches past the bridge fails here rather than shipping a
// second production owner.

const desktopRoot = resolve(fileURLToPath(new URL('../../../', import.meta.url)));
const settingsDir = join(desktopRoot, 'src', 'renderer', 'settings');

const RENDERER_PRICING_FILES = [
'pricing-settings-page.tsx',
'pricing-settings-view-model.ts',
'use-pricing-settings-controller.ts',
].map((name) => join(settingsDir, name));

// Import specifiers a renderer surface must never pull in. `/preload/` is
// intentionally absent: the renderer legitimately imports preload *types*
// (e.g. DesktopRuntimeHostRef) from the bridge contract.
const FORBIDDEN_IMPORT_SUBSTRINGS = [
'@maka/storage',
'root-authority',
'runtime-policy-stores',
'runtime-host-client',
'/main/',
];

// Source references that would mean the renderer bypassed the pricing bridge or
// reopened the retired usage bridge / legacy direct-Store channels.
const FORBIDDEN_SOURCE_SUBSTRINGS = [
'window.maka.usage',
'usage:pricing:list',
'usage:pricing:put',
'usage:pricing:reset',
];

describe('Pricing settings renderer boundary', () => {
it('imports no Store, embedded owner, or main-process client', () => {
const violations: string[] = [];
for (const path of RENDERER_PRICING_FILES) {
const source = readFileSync(path, 'utf8');
const name = relative(desktopRoot, path);
for (const match of source.matchAll(/from\s+['"]([^'"]+)['"]/g)) {
const imported = match[1] ?? '';
for (const forbidden of FORBIDDEN_IMPORT_SUBSTRINGS) {
if (imported.includes(forbidden)) violations.push(`${name}: imports ${imported}`);
}
}
for (const forbidden of FORBIDDEN_SOURCE_SUBSTRINGS) {
if (source.includes(forbidden)) violations.push(`${name}: references ${forbidden}`);
}
}
assert.deepEqual(violations, []);
});

it('reaches pricing through the settings.pricing bridge', () => {
const controller = readFileSync(
join(settingsDir, 'use-pricing-settings-controller.ts'),
'utf8',
);
assert.ok(
controller.includes('window.maka.settings.pricing'),
'the controller reaches pricing through window.maka.settings.pricing',
);
});
});
Loading
Loading