Files
openclaw/src/node-host/invoke-device-apps.test.ts
Peter Steinberger da69daeb72 feat(onboarding): recommend plugins and skills from installed apps (#109668)
* feat(onboarding): recommend plugins and skills from installed apps

Scan installed macOS apps during classic onboarding (TCC-free), gather
candidates from official catalogs + ClawHub search, let the configured
model pick genuine matches, and offer an opt-in multiselect install step.
Adds a device.apps node-host command (default-off sharing, Android-parity
envelope) so remote gateways can request a paired Mac's inventory, and a
wizard.appRecommendations kill switch. Custom setup-inference completions
no longer inherit the 32-token verification-probe output cap.

* feat(onboarding): recommend apps in guided flow

* fix(onboarding): harden app recommendations against ClawHub self-promotion

Third-party ClawHub skills are never pre-selected regardless of model tier
(publisher-controlled listing text reaches the matcher prompt and could
promote itself); their labels now say they install third-party code.
Installed-app scans follow symlinked .app bundles. Matcher output stays
bounded by the resolved model's own maxTokens budget (documented invariant).

* fix(onboarding): key official catalog candidates by resolved plugin id

Real catalog entries are package manifests without a top-level id; keying the
candidate map and channel/provider classification by entry.id collapsed the
whole official catalog into one undefined-keyed entry, so no official plugin
or channel was ever recommended. Regression test runs against the bundled
catalogs.

* fix(onboarding): satisfy lint, types, deadcode, and migration gates

Split the guided-onboarding test into a self-contained custodian suite to stay
under max-lines. Narrow app-recommendation exports (drop dead node-payload
normalizer, unexport internal types/helpers, route candidate tests through the
public API), replace map-spread with a helper, unexport device.apps result
types, add installedAppsSharing to node-host migration expectations, cast the
wizard multiselect mock, and regenerate the docs map.

* test(onboarding): register new live test in the shard classifier
2026-07-17 14:07:59 +01:00

56 lines
1.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { scanInstalledApps } from "../infra/installed-apps.js";
import { invokeDeviceApps } from "./invoke-device-apps.js";
const scan = vi.fn<typeof scanInstalledApps>(async () => ({
status: "ok",
apps: [
{ label: "Calendar", bundleId: "com.apple.iCal", path: "/System/Calendar.app", system: true },
{
label: "Notes App",
bundleId: "com.example.notes",
path: "/Applications/Notes.app",
system: false,
},
{ label: "Other", path: "/Applications/Other.app", system: false },
],
}));
describe("invokeDeviceApps", () => {
it("returns the typed privacy error while sharing is disabled", async () => {
await expect(
invokeDeviceApps({ sharingEnabled: false, platform: "darwin", scan }),
).resolves.toEqual({
ok: false,
code: "INSTALLED_APPS_SHARING_DISABLED",
message: "INSTALLED_APPS_SHARING_DISABLED: enable Installed Apps in node-host settings",
});
expect(scan).not.toHaveBeenCalled();
});
it("matches the Android count envelope with macOS app fields", async () => {
const result = await invokeDeviceApps({
sharingEnabled: true,
platform: "darwin",
paramsJSON: JSON.stringify({ query: "app", limit: 1, includeSystem: false }),
scan,
});
expect(result).toEqual({
ok: true,
payload: {
count: 1,
totalMatched: 1,
truncated: false,
apps: [
{
label: "Notes App",
bundleId: "com.example.notes",
path: "/Applications/Notes.app",
system: false,
},
],
},
});
});
});