Skip to content

Commit ab519d8

Browse files
committed
fix: restore HarmonyOS app inventory parity
1 parent 057011b commit ab519d8

4 files changed

Lines changed: 98 additions & 11 deletions

File tree

packages/platform-harmonyos/src/runtime.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from 'vitest';
1+
import { expect, test, vi } from 'vitest';
22
import type { PlatformRuntimeHost } from '@agent-device/contracts/platform';
33
import type { DeviceInfo } from '@agent-device/kernel/device';
44
import { createHarmonyPlatformRuntime } from './runtime.ts';
@@ -13,8 +13,10 @@ const device: DeviceInfo = {
1313
};
1414

1515
test('classifies the HarmonyOS runtime denominator', async () => {
16+
const listApps = vi.fn(async () => [{ id: 'com.example.application', name: 'application' }]);
1617
const host = {
1718
processTransports: { resolve: async () => ({ mode: 'local' as const }) },
19+
appInventory: { harmonyos: { listApps } },
1820
} as unknown as PlatformRuntimeHost;
1921
const binding = await createHarmonyPlatformRuntime(host).bind({
2022
device,
@@ -38,5 +40,10 @@ test('classifies the HarmonyOS runtime denominator', async () => {
3840
expect(facts.operations.ensureReady).toEqual({ available: true });
3941
expect(facts.operations.bootTarget).toMatchObject({ available: false });
4042
expect(facts.operations.bootTargetHeadless).toMatchObject({ available: false });
43+
expect(facts.operations.listApps).toEqual({ available: true });
4144
await expect(binding.operations.ensureReady?.({})).resolves.toMatchObject({ booted: true });
45+
await expect(binding.operations.listApps?.({ device, filter: 'all' })).resolves.toEqual([
46+
{ id: 'com.example.application', name: 'application' },
47+
]);
48+
expect(listApps).toHaveBeenCalledWith(device, 'all', expect.any(AbortSignal));
4249
});

packages/platform-harmonyos/src/runtime.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
PlatformRuntimeOwner,
66
} from '@agent-device/contracts/platform';
77
import { localRuntimeOwner } from '@agent-device/contracts/platform';
8+
import type { DeviceInfo } from '@agent-device/kernel/device';
89
import { createHarmonyAppLogRuntime } from './logs/runtime.ts';
910
import {
1011
createHarmonyScreenRecordingOperations,
@@ -34,7 +35,7 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor
3435
ensureReady: available,
3536
bootTarget: unavailable,
3637
bootTargetHeadless: unavailable,
37-
listApps: unavailable,
38+
listApps: available,
3839
},
3940
});
4041
};
@@ -61,6 +62,12 @@ export function createHarmonyPlatformRuntime(host: PlatformRuntimeHost): Platfor
6162
})
6263
: {}),
6364
ensureReady: async () => ({ ...request.device, booted: true }),
65+
listApps: async (input: { device: DeviceInfo; filter: 'all' | 'user-installed' }) =>
66+
await host.appInventory.harmonyos.listApps(
67+
input.device,
68+
input.filter,
69+
request.scope.signal,
70+
),
6471
}),
6572
[Symbol.asyncDispose]: async () => await logs[Symbol.asyncDispose](),
6673
}) satisfies DeviceBinding<PlatformRuntimeOperations>;

src/daemon/handlers/__tests__/session-capabilities.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,51 @@ test('capabilities excludes network when the runtime fact is unavailable', async
173173
expect(response.data?.availableCommands).not.toContain(PUBLIC_COMMANDS.network);
174174
});
175175

176+
test('capabilities includes apps for the available HarmonyOS runtime fact', async () => {
177+
const sessionName = 'harmony-capabilities';
178+
const sessionStore = makeSessionStore('agent-device-capabilities-harmony-');
179+
const harmonyDevice = {
180+
platform: 'harmonyos',
181+
id: 'harmony-capabilities',
182+
name: 'HarmonyOS device',
183+
kind: 'device',
184+
target: 'mobile',
185+
booted: true,
186+
} as const;
187+
sessionStore.set(sessionName, makeSession(sessionName, { device: harmonyDevice }));
188+
const runtime = createAdmissionRuntime({
189+
appLogAvailable: true,
190+
networkAvailable: false,
191+
appsAvailable: true,
192+
providerMode: 'local',
193+
});
194+
195+
const response = await withTargetDeviceResolutionScope(
196+
async (request) => (request.platform === 'harmonyos' ? [harmonyDevice] : []),
197+
async () =>
198+
await handleSessionCommands({
199+
req: {
200+
token: 't',
201+
session: sessionName,
202+
command: PUBLIC_COMMANDS.capabilities,
203+
positionals: [],
204+
flags: {},
205+
},
206+
sessionName,
207+
logPath: path.join(os.tmpdir(), 'daemon.log'),
208+
sessionStore,
209+
bindDevice: runtime.bindDevice,
210+
inspectFacts: runtime.inspectFacts,
211+
invoke: async () => ({ ok: true, data: {} }),
212+
}),
213+
);
214+
215+
expect(response?.ok).toBe(true);
216+
if (!response?.ok) return;
217+
expect(response.data?.availableCommands).toContain(PUBLIC_COMMANDS.apps);
218+
expect(runtime.inspectFacts).toHaveBeenCalledOnce();
219+
});
220+
176221
const APPS_UNAVAILABLE_CAPABILITY_CASES = [
177222
{ label: 'Linux', device: LINUX_DEVICE, providerMode: 'local' },
178223
{ label: 'Web', device: WEB_DESKTOP_DEVICE, providerMode: 'local' },

src/daemon/handlers/__tests__/session-inventory-harmonyos.test.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ const HARMONY_DEVICE = {
2323
};
2424
const available = { available: true } as const;
2525
const unavailable = { available: false, reason: 'unsupported-platform-leaf' } as const;
26+
const listAppsOperation: PlatformRuntimeOperations['listApps'] = vi.fn(async ({ filter }) =>
27+
filter === 'all'
28+
? [
29+
{ id: 'com.example.application', name: 'application' },
30+
{ id: 'com.ohos.settings', name: 'settings' },
31+
]
32+
: [{ id: 'com.example.application', name: 'application' }],
33+
);
34+
const ensureReady = vi.fn(async (device: typeof HARMONY_DEVICE) => ({
35+
...device,
36+
booted: true,
37+
}));
2638

2739
function runtimeFacts(): RuntimeFacts<PlatformRuntimeOperations> {
2840
return {
@@ -33,7 +45,7 @@ function runtimeFacts(): RuntimeFacts<PlatformRuntimeOperations> {
3345
appLogStart: available,
3446
appLogReattach: available,
3547
appLogCleanup: available,
36-
listApps: unavailable,
48+
listApps: { available: true },
3749
networkDump: unavailable,
3850
screenRecordingStart: unavailable,
3951
screenRecordingReattach: unavailable,
@@ -54,7 +66,7 @@ const bindDevice: BindDeviceRuntime = async (device, use) => {
5466
device,
5567
owner: localRuntimeOwner('harmonyos'),
5668
facts: runtimeFacts(),
57-
operations: { ensureReady: async () => device },
69+
operations: { ensureReady, listApps: listAppsOperation },
5870
[Symbol.asyncDispose]: async () => {},
5971
},
6072
use,
@@ -63,10 +75,12 @@ const bindDevice: BindDeviceRuntime = async (device, use) => {
6375

6476
beforeEach(() => {
6577
vi.mocked(inspectFacts).mockClear();
78+
vi.mocked(listAppsOperation).mockClear();
79+
vi.mocked(ensureReady).mockClear();
6680
bindCount = 0;
6781
});
6882

69-
async function listApps(): Promise<DaemonResponse | null> {
83+
async function listApps(appsFilter: 'all' | 'user-installed'): Promise<DaemonResponse | null> {
7084
const sessionName = 'harmony-apps';
7185
const sessionStore = makeSessionStore();
7286
sessionStore.set(sessionName, makeSession(sessionName, HARMONY_DEVICE));
@@ -75,7 +89,7 @@ async function listApps(): Promise<DaemonResponse | null> {
7589
session: sessionName,
7690
command: 'apps',
7791
positionals: [],
78-
flags: { appsFilter: 'all' },
92+
flags: { appsFilter },
7993
};
8094
return await handleSessionInventoryCommands({
8195
req,
@@ -86,11 +100,25 @@ async function listApps(): Promise<DaemonResponse | null> {
86100
});
87101
}
88102

89-
test('HarmonyOS apps remains fail-closed when the legacy capability rejected the leaf', async () => {
90-
await expect(listApps()).resolves.toMatchObject({
91-
ok: false,
92-
error: { code: 'UNSUPPORTED_OPERATION' },
103+
test.each([
104+
{
105+
appsFilter: 'user-installed' as const,
106+
expected: ['application (com.example.application)'],
107+
},
108+
{
109+
appsFilter: 'all' as const,
110+
expected: ['application (com.example.application)', 'settings (com.ohos.settings)'],
111+
},
112+
])('HarmonyOS apps preserves the $appsFilter response parity', async ({ appsFilter, expected }) => {
113+
await expect(listApps(appsFilter)).resolves.toMatchObject({
114+
ok: true,
115+
data: { apps: expected },
93116
});
94117
expect(inspectFacts).toHaveBeenCalledOnce();
95-
expect(bindCount).toBe(0);
118+
expect(bindCount).toBe(1);
119+
expect(ensureReady).toHaveBeenCalledOnce();
120+
expect(listAppsOperation).toHaveBeenCalledWith({
121+
device: expect.any(Object),
122+
filter: appsFilter,
123+
});
96124
});

0 commit comments

Comments
 (0)