@sero-ai/app-runtime 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/sero-bridge.ts +0 -7
- package/src/use-app-state.test.ts +56 -0
- package/src/use-app-state.ts +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sero-ai/app-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Hooks for Sero federated app modules — useAppState, useAppInfo, useAgentPrompt, useAppTools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/react": "^19.2.13",
|
|
25
25
|
"typescript": "^5.9.3",
|
|
26
|
-
"
|
|
26
|
+
"vitest": "^4.1.7",
|
|
27
|
+
"@sero-ai/common": "0.8.0"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
|
-
"typecheck": "tsc --noEmit"
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
30
32
|
}
|
|
31
33
|
}
|
package/src/sero-bridge.ts
CHANGED
|
@@ -11,8 +11,6 @@ import type {
|
|
|
11
11
|
AppToolResult,
|
|
12
12
|
AvailableContext,
|
|
13
13
|
ContextPreset,
|
|
14
|
-
GitActionResult,
|
|
15
|
-
GitManagerRequest,
|
|
16
14
|
SharedAvailableModelGroup,
|
|
17
15
|
SharedModelInfo,
|
|
18
16
|
WebAppActionResult,
|
|
@@ -50,10 +48,6 @@ export interface SeroAppControlBridge {
|
|
|
50
48
|
openFile(workspaceId: string, filePath: string): Promise<boolean>;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
export interface SeroGitAppBridge {
|
|
54
|
-
run(workspaceId: string, params: GitManagerRequest): Promise<GitActionResult>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
51
|
export interface SeroWebAppBridge {
|
|
58
52
|
run(workspaceId: string, params: WebAppRequest): Promise<WebAppActionResult>;
|
|
59
53
|
}
|
|
@@ -96,7 +90,6 @@ export interface SeroBridge {
|
|
|
96
90
|
appState: SeroWindowAppStateBridge;
|
|
97
91
|
appAgent: SeroAppAgentBridge;
|
|
98
92
|
appControl?: SeroAppControlBridge;
|
|
99
|
-
gitApp?: SeroGitAppBridge;
|
|
100
93
|
webApp?: SeroWebAppBridge;
|
|
101
94
|
editor?: SeroEditorBridge;
|
|
102
95
|
models?: SeroModelsBridge;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { applyDefaultState } from './use-app-state';
|
|
4
|
+
|
|
5
|
+
interface DemoState {
|
|
6
|
+
name: string;
|
|
7
|
+
count: number;
|
|
8
|
+
tags: string[];
|
|
9
|
+
nested: { open: boolean };
|
|
10
|
+
optional?: { from: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_STATE: DemoState = {
|
|
14
|
+
name: '',
|
|
15
|
+
count: 0,
|
|
16
|
+
tags: [],
|
|
17
|
+
nested: { open: false },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('applyDefaultState', () => {
|
|
21
|
+
it('takes the file’s values over the defaults', () => {
|
|
22
|
+
expect(applyDefaultState(DEFAULT_STATE, {
|
|
23
|
+
name: 'repo',
|
|
24
|
+
count: 3,
|
|
25
|
+
tags: ['a'],
|
|
26
|
+
nested: { open: true },
|
|
27
|
+
})).toEqual({ name: 'repo', count: 3, tags: ['a'], nested: { open: true } });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('keeps the default where the file disagrees about the type', () => {
|
|
31
|
+
const merged = applyDefaultState(DEFAULT_STATE, { name: 42, tags: 'nope' });
|
|
32
|
+
expect(merged.name).toBe('');
|
|
33
|
+
expect(merged.tags).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('fills in what the file leaves out', () => {
|
|
37
|
+
expect(applyDefaultState(DEFAULT_STATE, { name: 'repo' }).count).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// An `undefined` default says the field is optional, not that it must be
|
|
41
|
+
// absent. Enforcing it dropped the file's value on every read, which is how
|
|
42
|
+
// a whole feature's state could be written correctly and never arrive.
|
|
43
|
+
it('passes optional fields through when the default is undefined', () => {
|
|
44
|
+
const withOptional: DemoState = { ...DEFAULT_STATE, optional: undefined };
|
|
45
|
+
|
|
46
|
+
expect(applyDefaultState(withOptional, {
|
|
47
|
+
name: 'repo',
|
|
48
|
+
optional: { from: 'the file' },
|
|
49
|
+
}).optional).toEqual({ from: 'the file' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('leaves an absent optional field absent', () => {
|
|
53
|
+
const withOptional: DemoState = { ...DEFAULT_STATE, optional: undefined };
|
|
54
|
+
expect(applyDefaultState(withOptional, { name: 'repo' }).optional).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/use-app-state.ts
CHANGED
|
@@ -20,7 +20,19 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
20
20
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The state file is merged over the app's default state key by key, and the
|
|
25
|
+
* default wins wherever the two disagree in type — a malformed file cannot
|
|
26
|
+
* hand an app a number where it expects a string.
|
|
27
|
+
*
|
|
28
|
+
* An `undefined` default is the exception: it says the field is optional, not
|
|
29
|
+
* that it must be absent. Enforcing it would drop the file's value on every
|
|
30
|
+
* read, which is exactly what used to happen to any optional field an app
|
|
31
|
+
* declared as `undefined` in its default state.
|
|
32
|
+
*/
|
|
23
33
|
function normalizeStateValue(defaultValue: unknown, currentValue: unknown): unknown {
|
|
34
|
+
if (defaultValue === undefined) return currentValue;
|
|
35
|
+
|
|
24
36
|
if (Array.isArray(defaultValue)) {
|
|
25
37
|
return Array.isArray(currentValue) ? currentValue : defaultValue;
|
|
26
38
|
}
|
|
@@ -39,7 +51,11 @@ function normalizeStateValue(defaultValue: unknown, currentValue: unknown): unkn
|
|
|
39
51
|
return typeof currentValue === typeof defaultValue ? currentValue : defaultValue;
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Merge a state file's contents over an app's default state. Exported for its
|
|
56
|
+
* tests, not from the package entry point — it is not public API.
|
|
57
|
+
*/
|
|
58
|
+
export function applyDefaultState<T>(defaultState: T, current: unknown): T {
|
|
43
59
|
return normalizeStateValue(defaultState, current) as T;
|
|
44
60
|
}
|
|
45
61
|
|