@api-client/core 0.19.0 → 0.19.2

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.
Files changed (121) hide show
  1. package/build/src/decorators/observed.d.ts.map +1 -1
  2. package/build/src/decorators/observed.js +49 -8
  3. package/build/src/decorators/observed.js.map +1 -1
  4. package/build/src/mocking/ModelingMock.d.ts +2 -0
  5. package/build/src/mocking/ModelingMock.d.ts.map +1 -1
  6. package/build/src/mocking/ModelingMock.js +2 -0
  7. package/build/src/mocking/ModelingMock.js.map +1 -1
  8. package/build/src/mocking/lib/Ai.d.ts +11 -0
  9. package/build/src/mocking/lib/Ai.d.ts.map +1 -0
  10. package/build/src/mocking/lib/Ai.js +53 -0
  11. package/build/src/mocking/lib/Ai.js.map +1 -0
  12. package/build/src/modeling/ai/DataDomainDelta.d.ts +146 -0
  13. package/build/src/modeling/ai/DataDomainDelta.d.ts.map +1 -0
  14. package/build/src/modeling/ai/DataDomainDelta.js +729 -0
  15. package/build/src/modeling/ai/DataDomainDelta.js.map +1 -0
  16. package/build/src/modeling/ai/DomainSerialization.d.ts +20 -0
  17. package/build/src/modeling/ai/DomainSerialization.d.ts.map +1 -0
  18. package/build/src/modeling/ai/DomainSerialization.js +185 -0
  19. package/build/src/modeling/ai/DomainSerialization.js.map +1 -0
  20. package/build/src/modeling/ai/domain_response_schema.d.ts +806 -0
  21. package/build/src/modeling/ai/domain_response_schema.d.ts.map +1 -0
  22. package/build/src/modeling/ai/domain_response_schema.js +289 -0
  23. package/build/src/modeling/ai/domain_response_schema.js.map +1 -0
  24. package/build/src/modeling/ai/domain_tools.d.ts +68 -0
  25. package/build/src/modeling/ai/domain_tools.d.ts.map +1 -0
  26. package/build/src/modeling/ai/domain_tools.js +71 -0
  27. package/build/src/modeling/ai/domain_tools.js.map +1 -0
  28. package/build/src/modeling/ai/index.d.ts +10 -0
  29. package/build/src/modeling/ai/index.d.ts.map +1 -0
  30. package/build/src/modeling/ai/index.js +9 -0
  31. package/build/src/modeling/ai/index.js.map +1 -0
  32. package/build/src/modeling/ai/message_parser.d.ts +23 -0
  33. package/build/src/modeling/ai/message_parser.d.ts.map +1 -0
  34. package/build/src/modeling/ai/message_parser.js +93 -0
  35. package/build/src/modeling/ai/message_parser.js.map +1 -0
  36. package/build/src/modeling/ai/prompts/domain_system.d.ts +6 -0
  37. package/build/src/modeling/ai/prompts/domain_system.d.ts.map +1 -0
  38. package/build/src/modeling/ai/prompts/domain_system.js +80 -0
  39. package/build/src/modeling/ai/prompts/domain_system.js.map +1 -0
  40. package/build/src/modeling/ai/tools/DataDomain.tools.d.ts +25 -0
  41. package/build/src/modeling/ai/tools/DataDomain.tools.d.ts.map +1 -0
  42. package/build/src/modeling/ai/tools/DataDomain.tools.js +334 -0
  43. package/build/src/modeling/ai/tools/DataDomain.tools.js.map +1 -0
  44. package/build/src/modeling/ai/tools/Semantic.tools.d.ts +48 -0
  45. package/build/src/modeling/ai/tools/Semantic.tools.d.ts.map +1 -0
  46. package/build/src/modeling/ai/tools/Semantic.tools.js +36 -0
  47. package/build/src/modeling/ai/tools/Semantic.tools.js.map +1 -0
  48. package/build/src/modeling/ai/tools/config.d.ts +13 -0
  49. package/build/src/modeling/ai/tools/config.d.ts.map +1 -0
  50. package/build/src/modeling/ai/tools/config.js +2 -0
  51. package/build/src/modeling/ai/tools/config.js.map +1 -0
  52. package/build/src/modeling/ai/types.d.ts +302 -0
  53. package/build/src/modeling/ai/types.d.ts.map +1 -0
  54. package/build/src/modeling/ai/types.js +40 -0
  55. package/build/src/modeling/ai/types.js.map +1 -0
  56. package/build/src/models/AiMessage.d.ts +185 -0
  57. package/build/src/models/AiMessage.d.ts.map +1 -0
  58. package/build/src/models/AiMessage.js +203 -0
  59. package/build/src/models/AiMessage.js.map +1 -0
  60. package/build/src/models/AiSession.d.ts +80 -0
  61. package/build/src/models/AiSession.d.ts.map +1 -0
  62. package/build/src/models/AiSession.js +102 -0
  63. package/build/src/models/AiSession.js.map +1 -0
  64. package/build/src/models/kinds.d.ts +2 -0
  65. package/build/src/models/kinds.d.ts.map +1 -1
  66. package/build/src/models/kinds.js +2 -0
  67. package/build/src/models/kinds.js.map +1 -1
  68. package/build/src/sdk/AiSdk.d.ts +93 -0
  69. package/build/src/sdk/AiSdk.d.ts.map +1 -0
  70. package/build/src/sdk/AiSdk.js +348 -0
  71. package/build/src/sdk/AiSdk.js.map +1 -0
  72. package/build/src/sdk/RouteBuilder.d.ts +7 -0
  73. package/build/src/sdk/RouteBuilder.d.ts.map +1 -1
  74. package/build/src/sdk/RouteBuilder.js +18 -0
  75. package/build/src/sdk/RouteBuilder.js.map +1 -1
  76. package/build/src/sdk/Sdk.d.ts +2 -0
  77. package/build/src/sdk/Sdk.d.ts.map +1 -1
  78. package/build/src/sdk/Sdk.js +2 -0
  79. package/build/src/sdk/Sdk.js.map +1 -1
  80. package/build/src/sdk/SdkBase.d.ts +4 -0
  81. package/build/src/sdk/SdkBase.d.ts.map +1 -1
  82. package/build/src/sdk/SdkBase.js.map +1 -1
  83. package/build/src/sdk/SdkMock.d.ts +15 -0
  84. package/build/src/sdk/SdkMock.d.ts.map +1 -1
  85. package/build/src/sdk/SdkMock.js +118 -0
  86. package/build/src/sdk/SdkMock.js.map +1 -1
  87. package/build/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +3 -3
  89. package/src/decorators/observed.ts +51 -9
  90. package/src/mocking/ModelingMock.ts +2 -0
  91. package/src/mocking/lib/Ai.ts +71 -0
  92. package/src/modeling/ai/DataDomainDelta.ts +798 -0
  93. package/src/modeling/ai/DomainSerialization.ts +199 -0
  94. package/src/modeling/ai/domain_response_schema.ts +301 -0
  95. package/src/modeling/ai/domain_tools.ts +76 -0
  96. package/src/modeling/ai/message_parser.ts +101 -0
  97. package/src/modeling/ai/prompts/domain_system.ts +79 -0
  98. package/src/modeling/ai/readme.md +8 -0
  99. package/src/modeling/ai/tools/DataDomain.tools.ts +365 -0
  100. package/src/modeling/ai/tools/Semantic.tools.ts +38 -0
  101. package/src/modeling/ai/tools/config.ts +13 -0
  102. package/src/modeling/ai/tools/readme.md +3 -0
  103. package/src/modeling/ai/types.ts +306 -0
  104. package/src/models/AiMessage.ts +335 -0
  105. package/src/models/AiSession.ts +160 -0
  106. package/src/models/kinds.ts +2 -0
  107. package/src/sdk/AiSdk.ts +395 -0
  108. package/src/sdk/RouteBuilder.ts +27 -0
  109. package/src/sdk/Sdk.ts +3 -0
  110. package/src/sdk/SdkBase.ts +4 -0
  111. package/src/sdk/SdkMock.ts +185 -0
  112. package/tests/unit/decorators/observed_recursive.spec.ts +39 -0
  113. package/tests/unit/mocking/current/Ai.spec.ts +109 -0
  114. package/tests/unit/modeling/ai/DataDomainDelta.spec.ts +419 -0
  115. package/tests/unit/modeling/ai/DomainAiTools.spec.ts +29 -0
  116. package/tests/unit/modeling/ai/DomainSerialization.spec.ts +143 -0
  117. package/tests/unit/modeling/ai/message_parser.spec.ts +157 -0
  118. package/tests/unit/modeling/ai/tools/DataDomain.tools.spec.ts +64 -0
  119. package/tests/unit/modeling/ai/tools/Semantic.tools.spec.ts +55 -0
  120. package/tests/unit/models/AiMessage.spec.ts +216 -0
  121. package/tests/unit/models/AiSession.spec.ts +147 -0
@@ -0,0 +1,157 @@
1
+ import { test } from '@japa/runner'
2
+ import { parseAiChatMessage, detectReasoning } from '../../../../src/modeling/ai/message_parser.js'
3
+ import type { AiUserMessageSchema, AiModelMessageSchema } from '../../../../src/models/AiMessage.js'
4
+ import { AiMessageKind } from '../../../../src/models/kinds.js'
5
+ import { AiModelMessageWithDelta } from '../../../../src/modeling/ai/types.js'
6
+
7
+ test.group('Message Parser | parseAiChatMessage', () => {
8
+ test('returns user message as is', ({ assert }) => {
9
+ const userMsg: AiUserMessageSchema = {
10
+ kind: AiMessageKind,
11
+ key: 'msg1',
12
+ session: 'sess1',
13
+ createdAt: Date.now(),
14
+ updatedAt: Date.now(),
15
+ role: 'user',
16
+ state: 'complete',
17
+ text: 'Hello AI',
18
+ }
19
+
20
+ const result = parseAiChatMessage(userMsg)
21
+ assert.deepEqual(result, userMsg)
22
+ })
23
+
24
+ test('returns model message as is if state is not complete', ({ assert }) => {
25
+ const modelMsg: AiModelMessageSchema = {
26
+ kind: AiMessageKind,
27
+ key: 'msg2',
28
+ session: 'sess1',
29
+ createdAt: Date.now(),
30
+ updatedAt: Date.now(),
31
+ role: 'model',
32
+ state: 'loading',
33
+ text: '{"reasoning": "thinking...',
34
+ }
35
+
36
+ const result = parseAiChatMessage(modelMsg)
37
+ assert.deepEqual(result, modelMsg)
38
+ })
39
+
40
+ test('returns model message as is if JSON parsing fails', ({ assert }) => {
41
+ const modelMsg: AiModelMessageSchema = {
42
+ kind: AiMessageKind,
43
+ key: 'msg3',
44
+ session: 'sess1',
45
+ createdAt: Date.now(),
46
+ updatedAt: Date.now(),
47
+ role: 'model',
48
+ state: 'complete',
49
+ text: 'invalid json',
50
+ }
51
+
52
+ const result = parseAiChatMessage(modelMsg)
53
+ assert.deepEqual(result, modelMsg)
54
+ })
55
+
56
+ test('parses valid JSON text, sets delta and reasoning', ({ assert }) => {
57
+ const modelMsg: AiModelMessageSchema = {
58
+ kind: AiMessageKind,
59
+ key: 'msg4',
60
+ session: 'sess1',
61
+ createdAt: Date.now(),
62
+ updatedAt: Date.now(),
63
+ role: 'model',
64
+ state: 'complete',
65
+ text: JSON.stringify({
66
+ reasoning: 'I should add a new model',
67
+ delta: {
68
+ addedModels: [{ key: 'model1', name: 'Test Model' }],
69
+ },
70
+ }),
71
+ }
72
+
73
+ const result = parseAiChatMessage(modelMsg)
74
+ assert.equal(result.text, 'I should add a new model')
75
+ assert.isDefined((result as AiModelMessageWithDelta).delta)
76
+ assert.deepEqual((result as AiModelMessageWithDelta).delta!.addedModels, [{ key: 'model1', name: 'Test Model' }])
77
+ })
78
+
79
+ test('keeps original text if JSON has no reasoning', ({ assert }) => {
80
+ const originalJSON = JSON.stringify({
81
+ delta: {
82
+ addedModels: [{ key: 'model1', name: 'Test Model' }],
83
+ },
84
+ })
85
+ const modelMsg: AiModelMessageSchema = {
86
+ kind: AiMessageKind,
87
+ key: 'msg5',
88
+ session: 'sess1',
89
+ createdAt: Date.now(),
90
+ updatedAt: Date.now(),
91
+ role: 'model',
92
+ state: 'complete',
93
+ text: originalJSON,
94
+ }
95
+
96
+ const result = parseAiChatMessage(modelMsg)
97
+ assert.equal(result.text, originalJSON)
98
+ assert.isDefined((result as AiModelMessageWithDelta).delta)
99
+ })
100
+ })
101
+
102
+ test.group('Message Parser | detectReasoning', () => {
103
+ test('returns reasoning from valid JSON', ({ assert }) => {
104
+ const text = JSON.stringify({ reasoning: 'thinking process' })
105
+ const result = detectReasoning(text)
106
+ assert.equal(result, 'thinking process')
107
+ })
108
+
109
+ test('returns undefined from valid JSON without reasoning', ({ assert }) => {
110
+ const text = JSON.stringify({ other: 'data' })
111
+ const result = detectReasoning(text)
112
+ assert.isUndefined(result)
113
+ })
114
+
115
+ test('extracts reasoning from incomplete JSON string', ({ assert }) => {
116
+ const text = '{"reasoning": "this is some incomplete string'
117
+ const result = detectReasoning(text)
118
+ assert.equal(result, 'this is some incomplete string')
119
+ })
120
+
121
+ test('extracts reasoning with escaped characters in incomplete JSON', ({ assert }) => {
122
+ // Escaped newline and quotes within the incomplete JSON structure
123
+ const text = '{"reasoning": "line 1\\nline 2 with \\"quotes\\"'
124
+ const result = detectReasoning(text)
125
+ assert.equal(result, 'line 1\nline 2 with "quotes"')
126
+ })
127
+
128
+ test('extracts reasoning completely if it ends with quote in incomplete JSON', ({ assert }) => {
129
+ const text = '{"reasoning": "finished reasoning", "delta": '
130
+ const result = detectReasoning(text)
131
+ assert.equal(result, 'finished reasoning')
132
+ })
133
+
134
+ test('handles escaped trailing slashes gracefully', ({ assert }) => {
135
+ const text = '{"reasoning": "path\\\\to\\\\folder'
136
+ const result = detectReasoning(text)
137
+ assert.equal(result, 'path\\to\\folder')
138
+ })
139
+
140
+ test('returns undefined if "reasoning" key is missing in incomplete JSON', ({ assert }) => {
141
+ const text = '{"delta": {"added'
142
+ const result = detectReasoning(text)
143
+ assert.isUndefined(result)
144
+ })
145
+
146
+ test('returns undefined if colon is missing after "reasoning"', ({ assert }) => {
147
+ const text = '{"reasoning"'
148
+ const result = detectReasoning(text)
149
+ assert.isUndefined(result)
150
+ })
151
+
152
+ test('returns undefined if value quote is missing after "reasoning":', ({ assert }) => {
153
+ const text = '{"reasoning": '
154
+ const result = detectReasoning(text)
155
+ assert.isUndefined(result)
156
+ })
157
+ })
@@ -0,0 +1,64 @@
1
+ import { test } from '@japa/runner'
2
+ import { explain_schema, get_entity_details } from '../../../../../src/modeling/ai/tools/DataDomain.tools.js'
3
+ import { DataDomain } from '../../../../../src/index.js'
4
+ import type { ToolConfig } from '../../../../../src/modeling/ai/tools/config.js'
5
+
6
+ test.group('DataDomain.tools', () => {
7
+ test('explain_schema returns valid schemas', async ({ assert }) => {
8
+ const config = {} as ToolConfig
9
+ assert.isObject(await explain_schema(config, { dataType: 'string' }))
10
+ assert.isObject(await explain_schema(config, { dataType: 'number' }))
11
+ assert.isObject(await explain_schema(config, { dataType: 'boolean' }))
12
+ assert.isObject(await explain_schema(config, { dataType: 'date' }))
13
+ assert.isObject(await explain_schema(config, { dataType: 'datetime' }))
14
+ assert.isObject(await explain_schema(config, { dataType: 'time' }))
15
+ assert.isObject(await explain_schema(config, { dataType: 'binary' }))
16
+
17
+ try {
18
+ await explain_schema(config, { dataType: 'unsupported' })
19
+ assert.fail('Should throw error for unsupported type')
20
+ } catch (err: unknown) {
21
+ assert.equal((err as Error).message, 'Unsupported data type: unsupported')
22
+ }
23
+ })
24
+
25
+ test('get_entity_details handles errors', ({ assert }) => {
26
+ const config = {} as ToolConfig
27
+ assert.throws(() => get_entity_details(config, {}), 'No current domain provided.')
28
+
29
+ config.domain = new DataDomain()
30
+ assert.throws(() => get_entity_details(config, {}), 'Entity key is required.')
31
+
32
+ assert.throws(
33
+ () => get_entity_details(config, { entityKey: 'missing' }),
34
+ 'Entity with key missing not found in the current domain.'
35
+ )
36
+ })
37
+
38
+ test('get_entity_details successfully returns details for a valid entity', ({ assert }) => {
39
+ const domain = new DataDomain()
40
+ const model = domain.addModel()
41
+ domain.addEntity(model.key, { key: 'valid-entity', info: { name: 'Valid' } })
42
+ const config = { domain } as ToolConfig
43
+
44
+ const details = get_entity_details(config, { entityKey: 'valid-entity' })
45
+ assert.equal(details.key, 'valid-entity')
46
+ assert.equal(details.name, 'Valid')
47
+ assert.equal(details.modelKey, model.key)
48
+ })
49
+
50
+ test('get_entity_details handles entity without a parent model', ({ assert }) => {
51
+ const domain = new DataDomain()
52
+ const model = domain.addModel()
53
+ const entity = domain.addEntity(model.key, { key: 'orphan-entity', info: { name: 'Orphan' } })
54
+
55
+ // Mock getParentInstance
56
+ entity.getParentInstance = () => undefined
57
+
58
+ const config = { domain } as ToolConfig
59
+ assert.throws(
60
+ () => get_entity_details(config, { entityKey: 'orphan-entity' }),
61
+ 'Entity with key orphan-entity has no parent model.'
62
+ )
63
+ })
64
+ })
@@ -0,0 +1,55 @@
1
+ import { test } from '@japa/runner'
2
+ import { list_semantics, get_semantic_details } from '../../../../../src/modeling/ai/tools/Semantic.tools.js'
3
+ import { SemanticType, DataSemantics } from '../../../../../src/modeling/Semantics.js'
4
+ import { AiSemanticsConfig } from '../../../../../src/modeling/ai/Semantics.js'
5
+ import type { ToolConfig } from '../../../../../src/modeling/ai/tools/config.js'
6
+
7
+ test.group('Semantic.tools', () => {
8
+ const dummyConfig = {
9
+ logger: {
10
+ silly: () => {},
11
+ },
12
+ } as unknown as ToolConfig
13
+
14
+ test('list_semantics returns all available data semantics', ({ assert }) => {
15
+ const result = list_semantics(dummyConfig)
16
+ assert.isArray(result)
17
+ assert.lengthOf(result, Object.keys(DataSemantics).length)
18
+
19
+ // Check first item structure
20
+ const firstItem = result[0]
21
+ assert.property(firstItem, 'id')
22
+ assert.property(firstItem, 'displayName')
23
+ assert.property(firstItem, 'description')
24
+ assert.property(firstItem, 'scope')
25
+ assert.property(firstItem, 'category')
26
+ assert.property(firstItem, 'hasConfig')
27
+ // It should map from DataSemantics correctly
28
+ const firstKey = Object.keys(DataSemantics)[0] as SemanticType
29
+ assert.equal(firstItem.id, DataSemantics[firstKey].id)
30
+ assert.equal(firstItem.displayName, DataSemantics[firstKey].displayName)
31
+ })
32
+
33
+ test('get_semantic_details returns details', async ({ assert }) => {
34
+ const semanticId = SemanticType.Calculated
35
+ const result = await get_semantic_details(dummyConfig, { semanticId })
36
+
37
+ assert.equal(result.id, semanticId)
38
+ assert.equal(result.displayName, DataSemantics[semanticId].displayName)
39
+
40
+ const cnf = AiSemanticsConfig[semanticId] as { configSchema?: unknown }
41
+ assert.equal(result.configSchema, cnf.configSchema)
42
+ })
43
+
44
+ test('get_semantic_details throws if semanticId is missing', async ({ assert }) => {
45
+ await assert.rejects(async () => {
46
+ await get_semantic_details(dummyConfig, {})
47
+ }, 'Semantic ID is required.')
48
+ })
49
+
50
+ test('get_semantic_details throws if semanticId is invalid', async ({ assert }) => {
51
+ await assert.rejects(async () => {
52
+ await get_semantic_details(dummyConfig, { semanticId: 'invalid-id' })
53
+ }, 'Semantic with ID invalid-id not found')
54
+ })
55
+ })
@@ -0,0 +1,216 @@
1
+ import { test } from '@japa/runner'
2
+ import { AiUserMessage, AiModelMessage, AiMessageFactory } from '../../../src/models/AiMessage.js'
3
+ import { AiMessageKind } from '../../../src/models/kinds.js'
4
+
5
+ test.group('AiMessage | AiUserMessage', () => {
6
+ test('constants and static properties', ({ assert }) => {
7
+ assert.equal(AiUserMessage.Kind, AiMessageKind)
8
+ })
9
+
10
+ test('createSchema validates required session', ({ assert }) => {
11
+ assert.throws(() => AiUserMessage.createSchema({}), 'Session is required to create an AiMessage.')
12
+ })
13
+
14
+ test('createSchema builds valid user schema', ({ assert }) => {
15
+ const schema = AiUserMessage.createSchema({ session: 'session1', text: 'Hello' })
16
+ assert.isString(schema.key)
17
+ assert.isNotEmpty(schema.key)
18
+ assert.equal(schema.kind, AiMessageKind)
19
+ assert.equal(schema.session, 'session1')
20
+ assert.equal(schema.text, 'Hello')
21
+ assert.equal(schema.role, 'user')
22
+ assert.equal(schema.state, 'complete')
23
+ assert.equal(schema.createdAt, 0)
24
+ assert.equal(schema.updatedAt, 0)
25
+ assert.isUndefined(schema.deleted)
26
+ assert.isUndefined(schema.deletedInfo)
27
+ })
28
+
29
+ test('constructor builds AiUserMessage instance', ({ assert }) => {
30
+ const msg = new AiUserMessage({
31
+ key: 'msg1',
32
+ session: 'sess1',
33
+ text: 'User utterance',
34
+ createdAt: 10,
35
+ updatedAt: 20,
36
+ })
37
+
38
+ assert.equal(msg.key, 'msg1')
39
+ assert.equal(msg.session, 'sess1')
40
+ assert.equal(msg.role, 'user')
41
+ assert.equal(msg.state, 'complete')
42
+ assert.equal(msg.text, 'User utterance')
43
+ assert.equal(msg.createdAt, 10)
44
+ assert.equal(msg.updatedAt, 20)
45
+ assert.isFalse(msg.deleted)
46
+ })
47
+
48
+ test('toJSON correctly serializes an AiUserMessage', ({ assert }) => {
49
+ const msg = new AiUserMessage({
50
+ key: 'msg1',
51
+ session: 'sess1',
52
+ text: 'Message Data',
53
+ createdAt: 50,
54
+ updatedAt: 60,
55
+ deleted: true,
56
+ deletedInfo: { time: 100, byMe: true, user: 'user1' },
57
+ })
58
+
59
+ const json = msg.toJSON()
60
+ assert.deepEqual(json, {
61
+ kind: AiMessageKind,
62
+ key: 'msg1',
63
+ role: 'user',
64
+ session: 'sess1',
65
+ state: 'complete',
66
+ text: 'Message Data',
67
+ createdAt: 50,
68
+ updatedAt: 60,
69
+ deleted: true,
70
+ deletedInfo: { time: 100, byMe: true, user: 'user1' },
71
+ })
72
+ })
73
+ })
74
+
75
+ test.group('AiMessage | AiModelMessage', () => {
76
+ test('createSchema validates required session', ({ assert }) => {
77
+ assert.throws(() => AiModelMessage.createSchema({}), 'Session is required to create an AiMessage.')
78
+ })
79
+
80
+ test('createSchema builds valid model schema', ({ assert }) => {
81
+ const schema = AiModelMessage.createSchema({ session: 'session1', text: 'Response' })
82
+ assert.isString(schema.key)
83
+ assert.isNotEmpty(schema.key)
84
+ assert.equal(schema.kind, AiMessageKind)
85
+ assert.equal(schema.session, 'session1')
86
+ assert.equal(schema.text, 'Response')
87
+ assert.equal(schema.role, 'model')
88
+ assert.equal(schema.state, 'loading') // default
89
+ assert.equal(schema.thoughts, '')
90
+ assert.isUndefined(schema.applied)
91
+ assert.isUndefined(schema.deleted)
92
+ assert.isUndefined(schema.deletedInfo)
93
+ })
94
+
95
+ test('constructor builds AiModelMessage instance', ({ assert }) => {
96
+ const msg = new AiModelMessage({
97
+ key: 'msg1',
98
+ session: 'sess1',
99
+ text: 'Model utterance',
100
+ state: 'complete',
101
+ applied: true,
102
+ thoughts: 'Initial Thoughts',
103
+ createdAt: 10,
104
+ updatedAt: 20,
105
+ })
106
+
107
+ assert.equal(msg.key, 'msg1')
108
+ assert.equal(msg.session, 'sess1')
109
+ assert.equal(msg.role, 'model')
110
+ assert.equal(msg.state, 'complete')
111
+ assert.equal(msg.text, 'Model utterance')
112
+ assert.equal(msg.thoughts, 'Initial Thoughts')
113
+ assert.isTrue(msg.applied)
114
+ assert.equal(msg.createdAt, 10)
115
+ assert.equal(msg.updatedAt, 20)
116
+ assert.isFalse(msg.deleted)
117
+ })
118
+
119
+ test('toJSON correctly serializes an AiModelMessage', ({ assert }) => {
120
+ const msg = new AiModelMessage({
121
+ key: 'msg1',
122
+ session: 'sess1',
123
+ text: 'Message Data',
124
+ state: 'complete',
125
+ thoughts: 'Inner Monologue',
126
+ applied: false,
127
+ createdAt: 50,
128
+ updatedAt: 60,
129
+ deleted: true,
130
+ deletedInfo: { time: 100, byMe: true, user: 'user1' },
131
+ })
132
+
133
+ const json = msg.toJSON()
134
+ assert.deepEqual(json, {
135
+ kind: AiMessageKind,
136
+ key: 'msg1',
137
+ role: 'model',
138
+ session: 'sess1',
139
+ state: 'complete',
140
+ text: 'Message Data',
141
+ thoughts: 'Inner Monologue',
142
+ applied: false,
143
+ createdAt: 50,
144
+ updatedAt: 60,
145
+ deleted: true,
146
+ deletedInfo: { time: 100, byMe: true, user: 'user1' },
147
+ })
148
+ })
149
+
150
+ test('addThought safely appends thoughts', ({ assert }) => {
151
+ const msg = new AiModelMessage({ session: 's1' })
152
+ msg.addThought('Hello ')
153
+ msg.addThought('World!')
154
+ msg.addThought()
155
+ assert.equal(msg.thoughts, 'Hello World!')
156
+ })
157
+
158
+ test('addText safely appends text', ({ assert }) => {
159
+ const msg = new AiModelMessage({ session: 's1' })
160
+ msg.addText('Hello ')
161
+ msg.addText('World!')
162
+ msg.addText()
163
+ assert.equal(msg.text, 'Hello World!')
164
+ })
165
+
166
+ test('processMessage throws on invalid JSON', ({ assert }) => {
167
+ const msg = new AiModelMessage({ session: 's1', text: 'NOT JSON' })
168
+ assert.throws(() => msg.processMessage(), SyntaxError)
169
+ })
170
+
171
+ test('processMessage parses delta and reasoning', ({ assert }) => {
172
+ const msg = new AiModelMessage({
173
+ session: 's1',
174
+ text: JSON.stringify({
175
+ reasoning: 'I need to make models',
176
+ delta: { addedModels: [{ key: 'model1' }] },
177
+ }),
178
+ })
179
+ msg.processMessage()
180
+ assert.equal(msg.reasoning, 'I need to make models')
181
+ assert.isDefined(msg.delta)
182
+ assert.deepEqual(msg.delta!.addedModels, [{ key: 'model1' }])
183
+ })
184
+
185
+ test('processMessageSafe swallows errors', ({ assert }) => {
186
+ const msg = new AiModelMessage({ session: 's1', text: 'NOT JSON' })
187
+ msg.processMessageSafe() // should not throw
188
+ assert.isUndefined(msg.delta)
189
+ assert.isUndefined(msg.reasoning)
190
+ })
191
+
192
+ test('createEmpty generates loading stub', ({ assert }) => {
193
+ const msg = AiModelMessage.createEmpty('sess1')
194
+ assert.equal(msg.session, 'sess1')
195
+ assert.equal(msg.state, 'loading')
196
+ assert.equal(msg.role, 'model')
197
+ })
198
+ })
199
+
200
+ test.group('AiMessage | AiMessageFactory', () => {
201
+ test('fromSchema generates AiUserMessage when role=user', ({ assert }) => {
202
+ const schema = AiUserMessage.createSchema({ session: 'sess', text: 'Hello User' })
203
+ const msg = AiMessageFactory.fromSchema(schema)
204
+ assert.instanceOf(msg, AiUserMessage)
205
+ assert.equal(msg.role, 'user')
206
+ assert.equal(msg.text, 'Hello User')
207
+ })
208
+
209
+ test('fromSchema generates AiModelMessage when role=model', ({ assert }) => {
210
+ const schema = AiModelMessage.createSchema({ session: 'sess', text: 'Hello Model' })
211
+ const msg = AiMessageFactory.fromSchema(schema)
212
+ assert.instanceOf(msg, AiModelMessage)
213
+ assert.equal(msg.role, 'model')
214
+ assert.equal(msg.text, 'Hello Model')
215
+ })
216
+ })
@@ -0,0 +1,147 @@
1
+ import { test } from '@japa/runner'
2
+ import { AiSession } from '../../../src/models/AiSession.js'
3
+ import { AiSessionKind, AiMessageKind } from '../../../src/models/kinds.js'
4
+ import type { AiModelMessage } from '../../../src/models/AiMessage.js'
5
+
6
+ test.group('AiSession', () => {
7
+ test('constants and static properties', ({ assert }) => {
8
+ assert.equal(AiSession.Kind, AiSessionKind)
9
+
10
+ const session = new AiSession({ app: 'general' })
11
+ assert.equal(session.kind, AiSessionKind)
12
+ })
13
+
14
+ test('isSessionApp check valid and invalid apps', ({ assert }) => {
15
+ assert.isTrue(AiSession.isSessionApp('domain'))
16
+ assert.isTrue(AiSession.isSessionApp('api'))
17
+ assert.isTrue(AiSession.isSessionApp('general'))
18
+ assert.isFalse(AiSession.isSessionApp('unknown'))
19
+ assert.isFalse(AiSession.isSessionApp(null))
20
+ assert.isFalse(AiSession.isSessionApp(undefined))
21
+ })
22
+
23
+ test('createSchema validates required arguments', ({ assert }) => {
24
+ assert.throws(() => AiSession.createSchema({}), 'App is required to create an AiSession schema.')
25
+ // @ts-expect-error for testing invalid app
26
+ assert.throws(() => AiSession.createSchema({ app: 'invalid' }), 'Invalid app: invalid')
27
+ })
28
+
29
+ test('createSchema generates schema with defaults', ({ assert }) => {
30
+ const schema = AiSession.createSchema({ app: 'domain' })
31
+ assert.isString(schema.key)
32
+ assert.isNotEmpty(schema.key)
33
+ assert.equal(schema.kind, AiSessionKind)
34
+ assert.equal(schema.app, 'domain')
35
+ assert.equal(schema.title, '')
36
+ assert.isFalse(schema.pinned)
37
+ assert.isFalse(schema.userRenamed)
38
+ assert.equal(schema.createdAt, 0)
39
+ assert.equal(schema.updatedAt, 0)
40
+ assert.isUndefined(schema.deleted)
41
+ assert.isUndefined(schema.deletedInfo)
42
+ })
43
+
44
+ test('constructor builds AiSession instance', ({ assert }) => {
45
+ const session = new AiSession({
46
+ key: 'test-key',
47
+ app: 'api',
48
+ title: 'API Session',
49
+ pinned: true,
50
+ userRenamed: true,
51
+ createdAt: 100,
52
+ updatedAt: 200,
53
+ })
54
+
55
+ assert.equal(session.key, 'test-key')
56
+ assert.equal(session.app, 'api')
57
+ assert.equal(session.title, 'API Session')
58
+ assert.isTrue(session.pinned)
59
+ assert.isTrue(session.userRenamed)
60
+ assert.equal(session.createdAt, 100)
61
+ assert.equal(session.updatedAt, 200)
62
+ assert.isFalse(session.deleted)
63
+ assert.isUndefined(session.deletedInfo)
64
+ })
65
+
66
+ test('toJSON correctly serializes an AiSession', ({ assert }) => {
67
+ const session = new AiSession({
68
+ key: 'test-key',
69
+ app: 'general',
70
+ createdAt: 50,
71
+ updatedAt: 50,
72
+ deleted: true,
73
+ deletedInfo: {
74
+ time: 100,
75
+ byMe: true,
76
+ user: 'user1',
77
+ },
78
+ })
79
+ session.title = 'Test Title'
80
+
81
+ const json = session.toJSON()
82
+ assert.deepEqual(json, {
83
+ kind: AiSessionKind,
84
+ key: 'test-key',
85
+ app: 'general',
86
+ title: 'Test Title',
87
+ pinned: false,
88
+ userRenamed: false,
89
+ createdAt: 50,
90
+ updatedAt: 50,
91
+ deleted: true,
92
+ deletedInfo: {
93
+ time: 100,
94
+ byMe: true,
95
+ user: 'user1',
96
+ },
97
+ })
98
+ })
99
+
100
+ test('extractSessionTitle extracts sessionTitle when valid', ({ assert }) => {
101
+ const msg: AiModelMessage = {
102
+ kind: AiMessageKind,
103
+ key: 'msg1',
104
+ session: 'sess1',
105
+ role: 'model',
106
+ state: 'complete',
107
+ createdAt: 1,
108
+ updatedAt: 1,
109
+ text: JSON.stringify({ sessionTitle: 'New Generated Title' }),
110
+ } as unknown as AiModelMessage
111
+
112
+ const title = AiSession.extractSessionTitle(msg)
113
+ assert.equal(title, 'New Generated Title')
114
+ })
115
+
116
+ test('extractSessionTitle returns null for non-json response text', ({ assert }) => {
117
+ const msg: AiModelMessage = {
118
+ kind: AiMessageKind,
119
+ key: 'msg1',
120
+ session: 'sess1',
121
+ role: 'model',
122
+ state: 'complete',
123
+ createdAt: 1,
124
+ updatedAt: 1,
125
+ text: 'I am not JSON',
126
+ } as unknown as AiModelMessage
127
+
128
+ const title = AiSession.extractSessionTitle(msg)
129
+ assert.isNull(title)
130
+ })
131
+
132
+ test('extractSessionTitle returns null for JSON without sessionTitle', ({ assert }) => {
133
+ const msg: AiModelMessage = {
134
+ kind: AiMessageKind,
135
+ key: 'msg1',
136
+ session: 'sess1',
137
+ role: 'model',
138
+ state: 'complete',
139
+ createdAt: 1,
140
+ updatedAt: 1,
141
+ text: JSON.stringify({ otherField: 'value' }),
142
+ } as unknown as AiModelMessage
143
+
144
+ const title = AiSession.extractSessionTitle(msg)
145
+ assert.isNull(title)
146
+ })
147
+ })