@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sero-ai/app-runtime",
3
- "version": "0.2.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
- "@sero-ai/common": "0.4.0"
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
  }
@@ -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
+ });
@@ -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
- function applyDefaultState<T>(defaultState: T, current: unknown): T {
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