Skip to content
Merged
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
4 changes: 0 additions & 4 deletions .github/workflows/desktop-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@ on:
- "scripts/stage-desktop-python.mjs"
- "requirements/document-runtime.txt"
- "requirements/document-runtime-constraints.txt"
- "src/**"
- "skills/**"
- "ui/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "tsup.config.ts"
workflow_dispatch:

jobs:
Expand Down
35 changes: 26 additions & 9 deletions .github/workflows/tests-layered.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ jobs:
typecheck:
name: Typecheck
runs-on: ubuntu-latest
needs: prompt_contract
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand All @@ -56,7 +55,6 @@ jobs:
name: Unit
runs-on: ubuntu-latest
timeout-minutes: 10
needs: typecheck
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand All @@ -77,7 +75,6 @@ jobs:
integration:
name: Integration
runs-on: ubuntu-latest
needs: typecheck
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand All @@ -99,7 +96,29 @@ jobs:
e2e:
name: E2E
runs-on: ubuntu-latest
needs: typecheck
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: mkdir -p reports
- run: pnpm test:e2e:report
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-report-e2e
path: reports/e2e.json
if-no-files-found: error

web_smoke:
name: Web UI Smoke
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand All @@ -109,17 +128,15 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build:all
- name: Install browser for Web UI smoke
- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium
- run: mkdir -p reports
- run: pnpm test:e2e:report
- run: pnpm test:e2e:web -- --skip-build
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-report-e2e
name: test-report-web-smoke
path: |
reports/e2e.json
reports/web-ui-smoke.json
output/playwright/web-ui-smoke.png
output/playwright/web-ui-*.png
if-no-files-found: error
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ the shipped set and `docs/channels/UNSUPPORTED.md` for deferred ones.
| Database | better-sqlite3 (WAL mode, single file) |
| Logging | pino (structured JSON) |
| Scheduler | croner |
| Testing | vitest (targeted unit tests; explicit integration tests own real API calls) |
| Testing | Vitest (deterministic unit, integration, and E2E suites; provider availability is not a CI test) |
| LLM Layer | Vercel AI SDK (`ai`, `@ai-sdk/anthropic`, `@ai-sdk/openai`, `@ai-sdk/openai-compatible`) |
| Agent Runtime | Direct `src/core/brain-engine.ts` LLM/tool loop using `executeToolCalls` |
| Vector Memory | LanceDB (`@lancedb/lancedb`) |
Expand Down Expand Up @@ -405,14 +405,14 @@ the batch.
- Create circular dependencies between layers
- Skip verification steps in IMPLEMENTATION.md
- Modify IMPLEMENTATION.md without also committing the code changes
- Put real provider calls in explicit integration tests; unit tests mock external boundaries
- Put real provider or provider-availability calls in CI tests

## Testing

- **Framework:** vitest
- **Test files:** `src/**/*.test.ts` (224 files as of 2026-07-03; run vitest for the current assertion count)
- **Run:** `pnpm test`
- **Style:** Targeted unit tests mock external boundaries. Real provider tests are explicit integration tests and require credentials.
- **Test files:** deterministic unit, integration, and E2E tests under `src/` and `tests/`
- **Run:** the focused config and files that cover the change; use `pnpm test:ci` only when the whole deterministic stack is the subject
- **Style:** tests mock external provider boundaries. Provider availability is operational diagnostics, not a pass/fail code contract.
- Every module should have tests for happy path + error cases.

## When Stuck
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Thank you for improving OpenMozi. Contributions must preserve truthful runtime b
5. Run the relevant local tests and `pnpm verify:public-export`.
6. Open a focused pull request using the repository template and report only checks that actually ran.

GitHub Actions are intentionally disabled. Pull requests therefore require explicit local verification evidence; an empty check list is not proof that a change passed.
GitHub Actions report deterministic policy, type, unit, integration, and E2E checks. Contributors must still provide focused local verification for the changed behavior; a skipped or missing check is not proof that a change passed.

## Security and privacy

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening an issue or pull request.

## Releases

GitHub Actions are intentionally disabled. Releases are built and verified locally, then uploaded to [GitHub Releases](https://github.com/spytensor/openmozi/releases) with DMG, ZIP, SHA-256 checksums, and a release manifest. See [docs/RELEASE.md](docs/RELEASE.md).
GitHub Actions run deterministic policy, type, unit, integration, and E2E checks. The browser smoke runs after changes land on `main` or when triggered manually; it does not block ordinary pull requests. Releases are still built and verified locally, then uploaded to [GitHub Releases](https://github.com/spytensor/openmozi/releases) with DMG, ZIP, SHA-256 checksums, and a release manifest. See [docs/RELEASE.md](docs/RELEASE.md).

## Acknowledgments

Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release Process

OpenMozi releases are built and verified on a local macOS machine. GitHub is used only for source tags, the Release page, and artifact hosting. GitHub Actions are intentionally disabled and are not part of the release path.
OpenMozi releases are built and verified on a local macOS machine. GitHub Actions validate source changes, but they do not build or publish a release. GitHub Releases hosts the source tags and release artifacts produced by the local release process.

## Commands

Expand Down
9 changes: 5 additions & 4 deletions docs/WEB-UI-RUNTIME-UX-TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,12 @@ Each PR in this workstream must run:

- `git diff --check`
- targeted unit/UI tests for touched components
- `pnpm --filter mozi-ui test`
- relevant backend tests when runtime/API contracts change
- `pnpm build:all`
- `pnpm test:e2e:web -- --skip-build`
- real browser screenshots for changed surfaces
- the relevant typecheck or build when the changed boundary requires it
- a real browser check and screenshot only for changed UI behavior

The repository-level Web UI smoke runs on `main` or by manual dispatch. It is
not a blanket per-PR requirement and does not replace focused component tests.

For runtime/task UX changes, also run one real read-only task through
`http://127.0.0.1:9210`, capture:
Expand Down
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@
"test:unit:report": "vitest run --config vitest.unit.config.ts --reporter=default --reporter=json --outputFile=reports/unit.json",
"test:integration": "vitest run --config vitest.integration.config.ts",
"test:integration:report": "vitest run --config vitest.integration.config.ts --reporter=default --reporter=json --outputFile=reports/integration.json",
"test:provider-compat:smoke": "NO_UPDATE_NOTIFIER=1 PROVIDER_COMPAT_MODE=smoke pnpm exec vitest run --config vitest.integration.config.ts tests/integration/provider-compat-matrix.integration.test.ts",
"test:provider-compat:full": "NO_UPDATE_NOTIFIER=1 PROVIDER_COMPAT_MODE=full pnpm exec vitest run --config vitest.integration.config.ts tests/integration/provider-compat-matrix.integration.test.ts",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:e2e:report": "vitest run --config vitest.e2e.config.ts --reporter=default --reporter=json --outputFile=reports/e2e.json",
"test:e2e:web": "node scripts/web-ui-smoke.mjs",
"test:ci": "pnpm test:unit && pnpm test:integration && pnpm test:e2e && pnpm test:e2e:web",
"test:ci": "pnpm test:unit && pnpm build && pnpm test:integration && pnpm test:e2e",
"verify:prompt-contract": "node scripts/prompt-contract.mjs",
"verify:pre-merge": "pnpm verify:prompt-contract && pnpm typecheck && MOZI_E2E_LLM=scripted pnpm test:unit && pnpm build:all && pnpm test:integration && pnpm test:e2e && pnpm test:e2e:web --skip-build && pnpm gate:e2e",
"verify:pre-merge": "pnpm verify:prompt-contract && pnpm typecheck && pnpm test:unit && pnpm build && pnpm test:integration && pnpm test:e2e",
"replay:generate": "node scripts/failure-replay-generate.mjs",
"mozi": "node dist/cli.js",
"test:watch": "vitest",
Expand Down
9 changes: 5 additions & 4 deletions scripts/install-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ const MANIFESTS = ['package.json', 'ui/package.json', 'desktop/package.json'];

describe('install contract', () => {
it('declares the same Node and pnpm range in every workspace manifest', () => {
const expected = manifest('package.json').engines;
expect(expected?.node).toBe('>=22.12');
expect(expected?.pnpm).toBe('10.29.2');

for (const path of MANIFESTS) {
const engines = manifest(path).engines;
// The upper bound tracks better-sqlite3 (no prebuilds past 25.x); the
// lower bound tracks vite/vitest, which need 22.12+.
expect(engines?.node, path).toBe('>=22.12 <26');
expect(engines?.pnpm, path).toBe('10.29.2');
expect(engines, path).toEqual(expected);
}
});

Expand Down
118 changes: 0 additions & 118 deletions scripts/web-ui-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ const reportsDir = resolve('reports');
const diagnosticsLayoutScreenshotPath = join(outputDir, 'web-ui-diagnostics-layout.png');
const settingsLayoutScreenshotPath = join(outputDir, 'web-ui-settings-layout.png');
const chatSidebarScreenshotPath = join(outputDir, 'web-ui-chat-sidebar-contract.png');
const executionCollapsedScreenshotPath = join(outputDir, 'web-ui-execution-contract-collapsed.png');
const executionExpandedScreenshotPath = join(outputDir, 'web-ui-execution-contract-expanded.png');
const reportPath = join(reportsDir, 'web-ui-smoke.json');

mkdirSync(outputDir, { recursive: true });
Expand Down Expand Up @@ -117,7 +115,6 @@ try {
await completeOnboardingIfNeeded(page);
if (readmeScreenshots) await captureReadmeScreenshots(page);
const sidebarContract = await verifyChatFirstSidebarContract(page);
const executionContract = await verifyExecutionDisplayContract(page, sidebarContract.reusable_draft_session_id);
const paneScrollContract = await verifyPaneScrollContract(page);

await clickAccountMenuItem(page, 'Settings');
Expand Down Expand Up @@ -164,7 +161,6 @@ try {
},
service_status: serviceStatus,
sidebar_contract: sidebarContract,
execution_contract: executionContract,
pane_scroll_contract: paneScrollContract,
settings_layout_contract: settingsLayoutContract,
diagnostics_contract: diagnosticsContract,
Expand All @@ -173,8 +169,6 @@ try {
diagnostics_layout: diagnosticsLayoutScreenshotPath,
settings_layout: settingsLayoutScreenshotPath,
chat_sidebar_contract: chatSidebarScreenshotPath,
execution_contract_collapsed: executionCollapsedScreenshotPath,
execution_contract_expanded: executionExpandedScreenshotPath,
},
duration_ms: Date.now() - startedAt,
};
Expand Down Expand Up @@ -952,118 +946,6 @@ async function installWebSocketProbe(page) {
});
}

async function verifyExecutionDisplayContract(page, sessionId) {
await page.waitForFunction(() => window.__moziWebUiSmoke?.openSocketCount?.() > 0, { timeout: 30_000 });

const rawError =
'Error: web search failed — SEARCH1API_KEY environment variable is not set IMPORTANT: Do NOT answer this question from training data.';
await page.evaluate(({ rawError, sessionId }) => {
const now = Date.now();
const turnId = 'turn-web-ui-execution-contract';
const dispatch = window.__moziWebUiSmoke.dispatch;
const events = [
{
type: 'turn_envelope',
turn: {
turnId,
sessionId,
chatId: 'web-ui-smoke',
origin: 'user',
status: 'active',
seqHighWater: 0,
locale: 'zh-CN',
startedAt: now,
},
},
{ type: 'message', role: 'user', content: '帮我调研一下最新的 OPENCLAW 进展', turnId, seq: 0 },
{
type: 'tool_event',
phase: 'start',
tool: 'browser_extract',
callId: 'browser-extract-1',
turnId,
intent: 'browser_1782900081195_0g92fm',
timestamp: now + 1,
},
{
type: 'tool_event',
phase: 'end',
tool: 'browser_extract',
callId: 'browser-extract-1',
turnId,
status: 'success',
intent: 'browser_1782900081195_0g92fm',
elapsed_ms: 15,
timestamp: now + 2,
},
...[1, 2, 3].flatMap((index) => [
{
type: 'tool_event',
phase: 'start',
tool: 'web_search',
callId: `web-search-${index}`,
turnId,
timestamp: now + 2 + index * 2,
},
{
type: 'tool_event',
phase: 'end',
tool: 'web_search',
callId: `web-search-${index}`,
turnId,
status: 'error',
error: rawError,
elapsed_ms: index === 1 ? 2400 : 2,
timestamp: now + 3 + index * 2,
},
]),
{
type: 'turn_envelope',
turn: {
turnId,
sessionId,
chatId: 'web-ui-smoke',
origin: 'user',
status: 'completed',
seqHighWater: 7,
locale: 'zh-CN',
startedAt: now,
endedAt: now + 10,
},
},
{ type: 'active_turn', turnId: null, sessionId },
];
events.forEach((event) => dispatch(event));
}, { rawError, sessionId });

// Collapsed by default — a quiet one-line summary, no loud MOZI header, and
// neither work steps nor raw runtime detail visible until expanded.
await page.getByTestId('execution-summary').first().waitFor({ timeout: 10_000 });
const executionSummaryText = await page.getByTestId('execution-summary').first().textContent();
if (!/(View work|查看处理过程|Needs attention(?: \(3\))?|需要处理(?:(3))?)/.test(executionSummaryText ?? '')) {
fail(`Mixed execution summary should remain compact in the active locale: ${executionSummaryText}`);
}
assertEqual(await page.getByText('搜索公开资料需要处理').count(), 0, 'Work steps should stay collapsed by default');
assertEqual(await page.getByText('网络搜索').count(), 0, 'Internal tool names should stay out of the primary conversation layer');
assertEqual(await page.getByTestId('execution-timeline').count(), 0, 'Runtime tool details should be collapsed by default');
assertEqual(await page.getByText(/IMPORTANT: Do NOT answer/).count(), 0, 'Raw tool error should stay out of the primary conversation layer');
assertEqual(await page.getByText(/browser_1782900081195_0g92fm/).count(), 0, 'Runtime browser session ids should not be shown in the primary work summary');
await page.screenshot({ path: executionCollapsedScreenshotPath, fullPage: true });

// Expand the summary: user-facing work steps appear while raw provider text
// and internal runtime identifiers remain sanitized.
await page.getByTestId('execution-summary').first().click();
await page.getByText(/(Missing SEARCH1API_KEY.*3 times|缺少 SEARCH1API_KEY(重复 3 次))/).first().waitFor({ timeout: 10_000 });
assertEqual(await page.getByText(/IMPORTANT: Do NOT answer/).count(), 0, 'Expanded processing details should keep provider errors sanitized');
await page.screenshot({ path: executionExpandedScreenshotPath, fullPage: true });

return {
raw_tool_details_default_collapsed: true,
repeated_error_summary: 'localized and sanitized',
raw_error_rows_after_expand: 0,
};
}

function startRuntime({ moziHome, port }) {
const env = {
...process.env,
Expand Down
Loading
Loading