reviewflow 3.28.0 → 3.29.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/dashboard/index.html +61 -0
  3. package/dist/dashboard/modules/emberChat.d.ts +77 -0
  4. package/dist/dashboard/modules/emberChat.d.ts.map +1 -0
  5. package/dist/dashboard/modules/emberChat.js +175 -0
  6. package/dist/dashboard/modules/emberChat.js.map +1 -0
  7. package/dist/dashboard/styles.css +85 -0
  8. package/dist/main/routes.d.ts.map +1 -1
  9. package/dist/main/routes.js +32 -0
  10. package/dist/main/routes.js.map +1 -1
  11. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.d.ts +4 -0
  12. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.d.ts.map +1 -0
  13. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.js +4 -0
  14. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.js.map +1 -0
  15. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.d.ts +6 -0
  16. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.d.ts.map +1 -0
  17. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.js +5 -0
  18. package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.js.map +1 -0
  19. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.d.ts +7 -0
  20. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.d.ts.map +1 -0
  21. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.js +3 -0
  22. package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.js.map +1 -0
  23. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.d.ts +13 -0
  24. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.d.ts.map +1 -0
  25. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.js +35 -0
  26. package/dist/modules/ember-chat/entities/emberSession/emberSessionState.js.map +1 -0
  27. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.d.ts +26 -0
  28. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.d.ts.map +1 -0
  29. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.js +2 -0
  30. package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.js.map +1 -0
  31. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.d.ts +11 -0
  32. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.d.ts.map +1 -0
  33. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.js +2 -0
  34. package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.js.map +1 -0
  35. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.d.ts +15 -0
  36. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.d.ts.map +1 -0
  37. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js +61 -0
  38. package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js.map +1 -0
  39. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.d.ts +24 -0
  40. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.d.ts.map +1 -0
  41. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js +19 -0
  42. package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js.map +1 -0
  43. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.d.ts +13 -0
  44. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.d.ts.map +1 -0
  45. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.js +130 -0
  46. package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.js.map +1 -0
  47. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.d.ts +18 -0
  48. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.d.ts.map +1 -0
  49. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js +33 -0
  50. package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js.map +1 -0
  51. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.d.ts +20 -0
  52. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.d.ts.map +1 -0
  53. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js +28 -0
  54. package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js.map +1 -0
  55. package/dist/modules/ember-chat/services/emberSystemPrompt.d.ts +12 -0
  56. package/dist/modules/ember-chat/services/emberSystemPrompt.d.ts.map +1 -0
  57. package/dist/modules/ember-chat/services/emberSystemPrompt.js +29 -0
  58. package/dist/modules/ember-chat/services/emberSystemPrompt.js.map +1 -0
  59. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.d.ts +23 -0
  60. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.d.ts.map +1 -0
  61. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.js +23 -0
  62. package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.js.map +1 -0
  63. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.d.ts +36 -0
  64. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.d.ts.map +1 -0
  65. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.js +59 -0
  66. package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.js.map +1 -0
  67. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.d.ts +2 -0
  68. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.d.ts.map +1 -0
  69. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.js +124 -0
  70. package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.js.map +1 -0
  71. package/dist/tests/factories/emberMessage.factory.d.ts +5 -0
  72. package/dist/tests/factories/emberMessage.factory.d.ts.map +1 -0
  73. package/dist/tests/factories/emberMessage.factory.js +9 -0
  74. package/dist/tests/factories/emberMessage.factory.js.map +1 -0
  75. package/dist/tests/stubs/emberReadData.stub.d.ts +20 -0
  76. package/dist/tests/stubs/emberReadData.stub.d.ts.map +1 -0
  77. package/dist/tests/stubs/emberReadData.stub.js +31 -0
  78. package/dist/tests/stubs/emberReadData.stub.js.map +1 -0
  79. package/dist/tests/stubs/emberSessionTransport.stub.d.ts +18 -0
  80. package/dist/tests/stubs/emberSessionTransport.stub.d.ts.map +1 -0
  81. package/dist/tests/stubs/emberSessionTransport.stub.js +75 -0
  82. package/dist/tests/stubs/emberSessionTransport.stub.js.map +1 -0
  83. package/dist/tests/units/dashboard/modules/emberChat.test.d.ts +2 -0
  84. package/dist/tests/units/dashboard/modules/emberChat.test.d.ts.map +1 -0
  85. package/dist/tests/units/dashboard/modules/emberChat.test.js +59 -0
  86. package/dist/tests/units/dashboard/modules/emberChat.test.js.map +1 -0
  87. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.d.ts +2 -0
  88. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.d.ts.map +1 -0
  89. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.js +101 -0
  90. package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.js.map +1 -0
  91. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.d.ts +2 -0
  92. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.d.ts.map +1 -0
  93. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.js +24 -0
  94. package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.js.map +1 -0
  95. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.d.ts +2 -0
  96. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.d.ts.map +1 -0
  97. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.js +42 -0
  98. package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.js.map +1 -0
  99. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.d.ts +2 -0
  100. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.d.ts.map +1 -0
  101. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.js +75 -0
  102. package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.js.map +1 -0
  103. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.d.ts +2 -0
  104. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.d.ts.map +1 -0
  105. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.js +52 -0
  106. package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.js.map +1 -0
  107. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.d.ts +2 -0
  108. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.d.ts.map +1 -0
  109. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.js +27 -0
  110. package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.js.map +1 -0
  111. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.d.ts +2 -0
  112. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.d.ts.map +1 -0
  113. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.js +41 -0
  114. package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.js.map +1 -0
  115. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.d.ts +2 -0
  116. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.d.ts.map +1 -0
  117. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.js +46 -0
  118. package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.js.map +1 -0
  119. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.d.ts +2 -0
  120. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.d.ts.map +1 -0
  121. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.js +88 -0
  122. package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.js.map +1 -0
  123. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import Fastify from 'fastify';
3
+ import { emberChatRoutes } from '../../../../../modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js';
4
+ import { EmberSessionRegistry } from '../../../../../modules/ember-chat/usecases/emberSession/emberSessionRegistry.js';
5
+ import { StubEmberSessionTransportGateway } from '../../../../../tests/stubs/emberSessionTransport.stub.js';
6
+ import { StubEmberReadDataGateway } from '../../../../../tests/stubs/emberReadData.stub.js';
7
+ import { StubEnvironmentGateway } from '../../../../../tests/stubs/environment.stub.js';
8
+ import { createStubLogger } from '../../../../../tests/stubs/logger.stub.js';
9
+ const PROJECT_PATH = '/projects/alpha';
10
+ function parseSseEvents(body) {
11
+ return body
12
+ .split('\n')
13
+ .filter((line) => line.startsWith('data: '))
14
+ .map((line) => JSON.parse(line.slice('data: '.length)));
15
+ }
16
+ describe('emberChat routes', () => {
17
+ let application;
18
+ let transport;
19
+ let environment;
20
+ function buildApplication() {
21
+ transport = new StubEmberSessionTransportGateway();
22
+ transport.respondWith(async (question) => `Réponse à ${question}`);
23
+ environment = new StubEnvironmentGateway();
24
+ environment.setHasAnthropicApiKey(false);
25
+ const registry = new EmberSessionRegistry({
26
+ transport,
27
+ now: () => new Date('2026-05-28T10:00:00Z'),
28
+ idleTimeoutMs: 60_000,
29
+ });
30
+ const instance = Fastify();
31
+ void instance.register(emberChatRoutes, {
32
+ registry,
33
+ environment,
34
+ readData: new StubEmberReadDataGateway(),
35
+ projectPath: PROJECT_PATH,
36
+ now: () => new Date('2026-05-28T10:00:00Z'),
37
+ logger: createStubLogger(),
38
+ });
39
+ return instance;
40
+ }
41
+ beforeEach(async () => {
42
+ application = buildApplication();
43
+ await application.ready();
44
+ });
45
+ afterEach(async () => {
46
+ await application.close();
47
+ });
48
+ it('rejects an empty question with 400 and never spawns', async () => {
49
+ const response = await application.inject({
50
+ method: 'POST',
51
+ url: '/api/ember/ask',
52
+ payload: { question: '' },
53
+ });
54
+ expect(response.statusCode).toBe(400);
55
+ expect(transport.spawnCount).toBe(0);
56
+ });
57
+ it('streams chunk, status and end events for a valid question', async () => {
58
+ const response = await application.inject({
59
+ method: 'POST',
60
+ url: '/api/ember/ask',
61
+ payload: { question: 'Quel projet a le pire score ?' },
62
+ });
63
+ expect(response.statusCode).toBe(200);
64
+ expect(response.headers['content-type']).toContain('text/event-stream');
65
+ const events = parseSseEvents(response.body);
66
+ const statuses = events.filter((event) => event.type === 'status').map((event) => event.state);
67
+ const chunks = events.filter((event) => event.type === 'chunk').map((event) => event.text);
68
+ expect(statuses[0]).toBe('working');
69
+ expect(statuses.at(-1)).toBe('idle');
70
+ expect(chunks.join('')).toContain('Réponse');
71
+ });
72
+ it('emits an error event when the assistant is unreachable', async () => {
73
+ application = buildApplication();
74
+ transport.failSpawn();
75
+ await application.ready();
76
+ const response = await application.inject({
77
+ method: 'POST',
78
+ url: '/api/ember/ask',
79
+ payload: { question: 'Quel projet a le pire score ?' },
80
+ });
81
+ const events = parseSseEvents(response.body);
82
+ const errors = events.filter((event) => event.type === 'error');
83
+ expect(errors.length).toBeGreaterThan(0);
84
+ expect(String(errors[0].message)).toContain('INDISPONIBLE');
85
+ });
86
+ it('does not stream when an Anthropic API key is present', async () => {
87
+ application = buildApplication();
88
+ environment.setHasAnthropicApiKey(true);
89
+ await application.ready();
90
+ const response = await application.inject({
91
+ method: 'POST',
92
+ url: '/api/ember/ask',
93
+ payload: { question: 'Quel projet a le pire score ?' },
94
+ });
95
+ const events = parseSseEvents(response.body);
96
+ const errors = events.filter((event) => event.type === 'error');
97
+ expect(errors.length).toBeGreaterThan(0);
98
+ expect(transport.spawnCount).toBe(0);
99
+ });
100
+ });
101
+ //# sourceMappingURL=emberChat.routes.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberChat.routes.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/controllers/emberChat.routes.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberMessage.guard.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberMessage.guard.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/entities/emberMessage.guard.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { emberMessageGuard } from '../../../../../modules/ember-chat/entities/emberMessage/emberMessage.guard.js';
3
+ describe('emberMessageGuard', () => {
4
+ it('accepts a non-empty question', () => {
5
+ const result = emberMessageGuard.safeParse({ question: 'Quel projet a le pire score ?' });
6
+ expect(result.success).toBe(true);
7
+ });
8
+ it('rejects an empty question', () => {
9
+ const result = emberMessageGuard.safeParse({ question: '' });
10
+ expect(result.success).toBe(false);
11
+ });
12
+ it('rejects a whitespace-only question', () => {
13
+ const result = emberMessageGuard.safeParse({ question: ' ' });
14
+ expect(result.success).toBe(false);
15
+ });
16
+ it('trims surrounding whitespace from a valid question', () => {
17
+ const result = emberMessageGuard.safeParse({ question: ' bonjour ' });
18
+ expect(result.success).toBe(true);
19
+ if (result.success) {
20
+ expect(result.data.question).toBe('bonjour');
21
+ }
22
+ });
23
+ });
24
+ //# sourceMappingURL=emberMessage.guard.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberMessage.guard.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/entities/emberMessage.guard.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kEAAkE,CAAC;AAErG,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,+BAA+B,EAAE,CAAC,CAAC;QAE1F,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC;QAE7D,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAEhE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;QAExE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberSessionState.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberSessionState.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/entities/emberSessionState.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createIdleEmberSessionState } from '../../../../../modules/ember-chat/entities/emberSession/emberSessionState.js';
3
+ const IDLE_TIMEOUT_MS = 60_000;
4
+ describe('EmberSessionState', () => {
5
+ it('starts idle with no live process', () => {
6
+ const state = createIdleEmberSessionState();
7
+ expect(state.phase).toBe('idle');
8
+ });
9
+ it('becomes live when a question is asked', () => {
10
+ const state = createIdleEmberSessionState().onQuestion(new Date('2026-05-28T10:00:00Z'));
11
+ expect(state.phase).toBe('live');
12
+ });
13
+ it('stays live after an answer completes', () => {
14
+ const state = createIdleEmberSessionState()
15
+ .onQuestion(new Date('2026-05-28T10:00:00Z'))
16
+ .onAnswerDone(new Date('2026-05-28T10:00:05Z'));
17
+ expect(state.phase).toBe('live');
18
+ });
19
+ it('releases to idle when inactivity exceeds the timeout', () => {
20
+ const state = createIdleEmberSessionState()
21
+ .onQuestion(new Date('2026-05-28T10:00:00Z'))
22
+ .onAnswerDone(new Date('2026-05-28T10:00:05Z'))
23
+ .onIdleTick(new Date('2026-05-28T10:02:00Z'), IDLE_TIMEOUT_MS);
24
+ expect(state.phase).toBe('idle');
25
+ });
26
+ it('stays live when inactivity is within the timeout', () => {
27
+ const state = createIdleEmberSessionState()
28
+ .onQuestion(new Date('2026-05-28T10:00:00Z'))
29
+ .onAnswerDone(new Date('2026-05-28T10:00:05Z'))
30
+ .onIdleTick(new Date('2026-05-28T10:00:30Z'), IDLE_TIMEOUT_MS);
31
+ expect(state.phase).toBe('live');
32
+ });
33
+ it('reports needing a process when idle and a question arrives', () => {
34
+ const idle = createIdleEmberSessionState();
35
+ expect(idle.needsProcess()).toBe(true);
36
+ });
37
+ it('does not need a new process when already live', () => {
38
+ const live = createIdleEmberSessionState().onQuestion(new Date('2026-05-28T10:00:00Z'));
39
+ expect(live.needsProcess()).toBe(false);
40
+ });
41
+ });
42
+ //# sourceMappingURL=emberSessionState.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberSessionState.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/entities/emberSessionState.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,2BAA2B,EAAE,MAAM,iEAAiE,CAAC;AAE9G,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,KAAK,GAAG,2BAA2B,EAAE,CAAC;QAE5C,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,KAAK,GAAG,2BAA2B,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC;QAEzF,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,KAAK,GAAG,2BAA2B,EAAE;aACxC,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;aAC5C,YAAY,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC;QAElD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,KAAK,GAAG,2BAA2B,EAAE;aACxC,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;aAC5C,YAAY,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;aAC9C,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,EAAE,eAAe,CAAC,CAAC;QAEjE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,KAAK,GAAG,2BAA2B,EAAE;aACxC,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;aAC5C,YAAY,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC;aAC9C,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,EAAE,eAAe,CAAC,CAAC;QAEjE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,IAAI,GAAG,2BAA2B,EAAE,CAAC;QAE3C,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG,2BAA2B,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC;QAExF,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberReadData.composite.gateway.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberReadData.composite.gateway.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { InMemoryStatsGateway } from '../../../../../tests/stubs/stats.stub.js';
3
+ import { InMemoryInsightsGateway } from '../../../../../tests/stubs/insights.stub.js';
4
+ import { InMemoryReviewRequestTrackingGateway } from '../../../../../tests/stubs/reviewRequestTracking.stub.js';
5
+ import { ProjectStatsFactory, ReviewStatsFactory } from '../../../../../tests/factories/projectStats.factory.js';
6
+ import { EmberReadDataCompositeGateway } from '../../../../../modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js';
7
+ import { createWorktreePath } from '../../../../../modules/worktree-management/entities/worktree/worktree.js';
8
+ class StubWorktreeGateway {
9
+ entries;
10
+ constructor(entries) {
11
+ this.entries = entries;
12
+ }
13
+ async ensure() {
14
+ throw new Error('not used');
15
+ }
16
+ async remove() {
17
+ throw new Error('not used');
18
+ }
19
+ async list() {
20
+ return this.entries;
21
+ }
22
+ async exists() {
23
+ return false;
24
+ }
25
+ }
26
+ const PROJECT_PATH = '/projects/alpha';
27
+ function buildComposite() {
28
+ const stats = new InMemoryStatsGateway();
29
+ const insights = new InMemoryInsightsGateway();
30
+ const tracking = new InMemoryReviewRequestTrackingGateway();
31
+ const worktree = new StubWorktreeGateway([]);
32
+ const composite = new EmberReadDataCompositeGateway({
33
+ statsGateway: stats,
34
+ insightsGateway: insights,
35
+ trackingGateway: tracking,
36
+ worktreeGateway: worktree,
37
+ });
38
+ return { composite, stats };
39
+ }
40
+ describe('EmberReadDataCompositeGateway', () => {
41
+ it('delegates reviewScores to the existing stats gateway', async () => {
42
+ const { composite, stats } = buildComposite();
43
+ const projectStats = ProjectStatsFactory.withReviews([
44
+ ReviewStatsFactory.create({ mrNumber: 42, score: 3 }),
45
+ ]);
46
+ stats.saveProjectStats(PROJECT_PATH, projectStats);
47
+ const result = await composite.reviewScores(PROJECT_PATH);
48
+ expect(result?.reviews[0].mrNumber).toBe(42);
49
+ });
50
+ it('returns null review scores when no stats exist for the project', async () => {
51
+ const { composite } = buildComposite();
52
+ const result = await composite.reviewScores('/projects/unknown');
53
+ expect(result).toBeNull();
54
+ });
55
+ it('delegates worktrees to the existing worktree gateway list', async () => {
56
+ const stats = new InMemoryStatsGateway();
57
+ const insights = new InMemoryInsightsGateway();
58
+ const tracking = new InMemoryReviewRequestTrackingGateway();
59
+ const entry = {
60
+ identity: { platform: 'github', projectPath: PROJECT_PATH, mrNumber: 7 },
61
+ path: createWorktreePath('/worktrees/alpha-7'),
62
+ mtime: new Date('2026-05-28T10:00:00Z'),
63
+ };
64
+ const composite = new EmberReadDataCompositeGateway({
65
+ statsGateway: stats,
66
+ insightsGateway: insights,
67
+ trackingGateway: tracking,
68
+ worktreeGateway: new StubWorktreeGateway([entry]),
69
+ });
70
+ const result = await composite.worktrees();
71
+ expect(result).toHaveLength(1);
72
+ expect(result[0].identity.mrNumber).toBe(7);
73
+ });
74
+ });
75
+ //# sourceMappingURL=emberReadData.composite.gateway.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberReadData.composite.gateway.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberStreamJson.parser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberStreamJson.parser.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseStreamJsonEvent, extractText, isTurnComplete, } from '../../../../../modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js';
3
+ describe('parseStreamJsonEvent', () => {
4
+ it('parses a JSON object line into an event', () => {
5
+ expect(parseStreamJsonEvent('{"type":"result"}')).toEqual({ type: 'result' });
6
+ });
7
+ it('returns null for a non-object JSON value', () => {
8
+ expect(parseStreamJsonEvent('42')).toBeNull();
9
+ expect(parseStreamJsonEvent('null')).toBeNull();
10
+ });
11
+ it('returns null for an invalid JSON line', () => {
12
+ expect(parseStreamJsonEvent('{not json')).toBeNull();
13
+ expect(parseStreamJsonEvent('')).toBeNull();
14
+ });
15
+ });
16
+ describe('extractText', () => {
17
+ it('reads a top-level text field', () => {
18
+ expect(extractText({ text: 'hello' })).toBe('hello');
19
+ });
20
+ it('reads a streamed delta text', () => {
21
+ expect(extractText({ delta: { text: 'world' } })).toBe('world');
22
+ });
23
+ it('joins text parts from a message content array', () => {
24
+ expect(extractText({
25
+ message: { content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }] },
26
+ })).toBe('ab');
27
+ });
28
+ it('ignores non-text content parts', () => {
29
+ expect(extractText({
30
+ message: { content: [{ type: 'tool_use' }, { type: 'text', text: 'kept' }] },
31
+ })).toBe('kept');
32
+ });
33
+ it('returns null when content holds no text', () => {
34
+ expect(extractText({ message: { content: [{ type: 'tool_use' }] } })).toBeNull();
35
+ });
36
+ it('returns null when no text is present', () => {
37
+ expect(extractText({ type: 'result' })).toBeNull();
38
+ });
39
+ });
40
+ describe('isTurnComplete', () => {
41
+ it('is true on a result event', () => {
42
+ expect(isTurnComplete({ type: 'result' })).toBe(true);
43
+ });
44
+ it('is true on a message_stop event', () => {
45
+ expect(isTurnComplete({ type: 'message_stop' })).toBe(true);
46
+ });
47
+ it('is false on any other event', () => {
48
+ expect(isTurnComplete({ type: 'message_delta' })).toBe(false);
49
+ expect(isTurnComplete({})).toBe(false);
50
+ });
51
+ });
52
+ //# sourceMappingURL=emberStreamJson.parser.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberStreamJson.parser.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberStatus.presenter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberStatus.presenter.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { EmberStatusPresenter } from '../../../../../modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js';
3
+ const presenter = new EmberStatusPresenter();
4
+ describe('EmberStatusPresenter', () => {
5
+ it('maps the working status to the working avatar state with a French announcement', () => {
6
+ const viewModel = presenter.present({ kind: 'status', status: 'working' });
7
+ expect(viewModel.avatarState).toBe('working');
8
+ expect(viewModel.liveRegionText).toContain('Ember');
9
+ expect(viewModel.unavailableMessage).toBeNull();
10
+ });
11
+ it('maps the idle status to the idle avatar state', () => {
12
+ const viewModel = presenter.present({ kind: 'status', status: 'idle' });
13
+ expect(viewModel.avatarState).toBe('idle');
14
+ });
15
+ it('announces the answer text on a chunk-complete event', () => {
16
+ const viewModel = presenter.present({ kind: 'answer', text: 'Le pire score concerne la MR 42.' });
17
+ expect(viewModel.liveRegionText).toBe('Le pire score concerne la MR 42.');
18
+ expect(viewModel.avatarState).toBe('idle');
19
+ });
20
+ it('maps an error to the error avatar state with the French unavailable message', () => {
21
+ const viewModel = presenter.present({ kind: 'error' });
22
+ expect(viewModel.avatarState).toBe('error');
23
+ expect(viewModel.unavailableMessage).toBe('// EMBER INDISPONIBLE — réessayer');
24
+ expect(viewModel.liveRegionText).toContain('indisponible');
25
+ });
26
+ });
27
+ //# sourceMappingURL=emberStatus.presenter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberStatus.presenter.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6EAA6E,CAAC;AAEnH,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAE7C,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,gFAAgF,EAAE,GAAG,EAAE;QACxF,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAE3E,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACpD,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAExE,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,kCAAkC,EAAE,CAAC,CAAC;QAElG,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QAC1E,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACrF,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAEvD,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;QAC/E,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberSystemPrompt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberSystemPrompt.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/services/emberSystemPrompt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildEmberSystemPrompt } from '../../../../../modules/ember-chat/services/emberSystemPrompt.js';
3
+ import { ProjectStatsFactory, ReviewStatsFactory } from '../../../../../tests/factories/projectStats.factory.js';
4
+ const EMPTY_GROUNDING = {
5
+ reviewScores: null,
6
+ insights: null,
7
+ jobHistory: null,
8
+ worktrees: [],
9
+ };
10
+ describe('buildEmberSystemPrompt', () => {
11
+ it('names the four review-data sources as the only data sources', () => {
12
+ const prompt = buildEmberSystemPrompt(EMPTY_GROUNDING);
13
+ expect(prompt).toContain('reviewScores');
14
+ expect(prompt).toContain('insights');
15
+ expect(prompt).toContain('jobHistory');
16
+ expect(prompt).toContain('worktrees');
17
+ });
18
+ it('instructs to decline rather than invent when asked outside review data', () => {
19
+ const prompt = buildEmberSystemPrompt(EMPTY_GROUNDING);
20
+ expect(prompt.toLowerCase()).toContain('review');
21
+ expect(prompt).toMatch(/ne sait répondre|ne sais pas|reviews/i);
22
+ });
23
+ it('instructs that writing arrives in Phase B and to perform no writes', () => {
24
+ const prompt = buildEmberSystemPrompt(EMPTY_GROUNDING);
25
+ expect(prompt).toContain('Phase B');
26
+ expect(prompt.toLowerCase()).toContain('lecture seule');
27
+ });
28
+ it('identifies the assistant as Ember', () => {
29
+ expect(buildEmberSystemPrompt(EMPTY_GROUNDING)).toContain('Ember');
30
+ });
31
+ it('embeds the actual review data so the answer is grounded in it', () => {
32
+ const grounding = {
33
+ ...EMPTY_GROUNDING,
34
+ reviewScores: ProjectStatsFactory.withReviews([
35
+ ReviewStatsFactory.create({ mrNumber: 42, score: 3, blocking: 4, warnings: 1 }),
36
+ ]),
37
+ };
38
+ expect(buildEmberSystemPrompt(grounding)).toContain('42');
39
+ });
40
+ });
41
+ //# sourceMappingURL=emberSystemPrompt.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberSystemPrompt.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/services/emberSystemPrompt.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,oDAAoD,CAAC;AAE5F,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAEpG,MAAM,eAAe,GAAmB;IACtC,YAAY,EAAE,IAAI;IAClB,QAAQ,EAAE,IAAI;IACd,UAAU,EAAE,IAAI;IAChB,SAAS,EAAE,EAAE;CACd,CAAC;AAEF,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,MAAM,GAAG,sBAAsB,CAAC,eAAe,CAAC,CAAC;QACvD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACrC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QACvC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wEAAwE,EAAE,GAAG,EAAE;QAChF,MAAM,MAAM,GAAG,sBAAsB,CAAC,eAAe,CAAC,CAAC;QACvD,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,uCAAuC,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,MAAM,GAAG,sBAAsB,CAAC,eAAe,CAAC,CAAC;QACvD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,sBAAsB,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,SAAS,GAAmB;YAChC,GAAG,eAAe;YAClB,YAAY,EAAE,mBAAmB,CAAC,WAAW,CAAC;gBAC5C,kBAAkB,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;aAChF,CAAC;SACH,CAAC;QAEF,MAAM,CAAC,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=askEmber.usecase.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"askEmber.usecase.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { StubEmberSessionTransportGateway } from '../../../../../tests/stubs/emberSessionTransport.stub.js';
3
+ import { StubEmberReadDataGateway } from '../../../../../tests/stubs/emberReadData.stub.js';
4
+ import { StubEnvironmentGateway } from '../../../../../tests/stubs/environment.stub.js';
5
+ import { EmberSessionRegistry } from '../../../../../modules/ember-chat/usecases/emberSession/emberSessionRegistry.js';
6
+ import { askEmber } from '../../../../../modules/ember-chat/usecases/askEmber/askEmber.usecase.js';
7
+ const PROJECT_PATH = '/projects/alpha';
8
+ function buildDeps(options) {
9
+ const transport = new StubEmberSessionTransportGateway();
10
+ if (options.failSpawn === true) {
11
+ transport.failSpawn();
12
+ }
13
+ const registry = new EmberSessionRegistry({
14
+ transport,
15
+ now: () => new Date('2026-05-28T10:00:00Z'),
16
+ idleTimeoutMs: 60_000,
17
+ });
18
+ const environment = new StubEnvironmentGateway();
19
+ environment.setHasAnthropicApiKey(options.hasApiKey);
20
+ const readData = new StubEmberReadDataGateway();
21
+ return {
22
+ registry,
23
+ environment,
24
+ readData,
25
+ projectPath: PROJECT_PATH,
26
+ now: () => new Date('2026-05-28T10:00:00Z'),
27
+ };
28
+ }
29
+ describe('askEmber', () => {
30
+ it('returns a streaming result for a valid question with no API key', async () => {
31
+ const deps = buildDeps({ hasApiKey: false });
32
+ const result = await askEmber({ question: 'Quel projet a le pire score ?' }, deps);
33
+ expect(result.status).toBe('streaming');
34
+ });
35
+ it('prevents the billing regression when an Anthropic API key is present', async () => {
36
+ const deps = buildDeps({ hasApiKey: true });
37
+ const result = await askEmber({ question: 'Quel projet a le pire score ?' }, deps);
38
+ expect(result.status).toBe('billing-regression-prevented');
39
+ });
40
+ it('returns unavailable when the transport cannot spawn', async () => {
41
+ const deps = buildDeps({ hasApiKey: false, failSpawn: true });
42
+ const result = await askEmber({ question: 'Quel projet a le pire score ?' }, deps);
43
+ expect(result.status).toBe('unavailable');
44
+ });
45
+ });
46
+ //# sourceMappingURL=askEmber.usecase.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"askEmber.usecase.test.js","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,gCAAgC,EAAE,MAAM,6CAA6C,CAAC;AAC/F,OAAO,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,oEAAoE,CAAC;AAC1G,OAAO,EAAE,QAAQ,EAAE,MAAM,4DAA4D,CAAC;AAEtF,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC,SAAS,SAAS,CAAC,OAAoD;IAOrE,MAAM,SAAS,GAAG,IAAI,gCAAgC,EAAE,CAAC;IACzD,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAC/B,SAAS,CAAC,SAAS,EAAE,CAAC;IACxB,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,oBAAoB,CAAC;QACxC,SAAS;QACT,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC;QAC3C,aAAa,EAAE,MAAM;KACtB,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,IAAI,sBAAsB,EAAE,CAAC;IACjD,WAAW,CAAC,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,IAAI,wBAAwB,EAAE,CAAC;IAChD,OAAO;QACL,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,WAAW,EAAE,YAAY;QACzB,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,sBAAsB,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAE7C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,EAAE,QAAQ,EAAE,+BAA+B,EAAE,EAAE,IAAI,CAAC,CAAC;QAEnF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE5C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,EAAE,QAAQ,EAAE,+BAA+B,EAAE,EAAE,IAAI,CAAC,CAAC;QAEnF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9D,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,EAAE,QAAQ,EAAE,+BAA+B,EAAE,EAAE,IAAI,CAAC,CAAC;QAEnF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=emberSessionRegistry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emberSessionRegistry.test.d.ts","sourceRoot":"","sources":["../../../../../../src/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { StubEmberSessionTransportGateway } from '../../../../../tests/stubs/emberSessionTransport.stub.js';
3
+ import { EmberSessionRegistry } from '../../../../../modules/ember-chat/usecases/emberSession/emberSessionRegistry.js';
4
+ const SYSTEM_PROMPT = 'you are ember';
5
+ const PROJECT_PATH = '/projects/alpha';
6
+ function drain(subscribe) {
7
+ return new Promise((resolve) => {
8
+ let answer = '';
9
+ const statuses = [];
10
+ subscribe({
11
+ onStatus: (state) => statuses.push(state),
12
+ onChunk: (text) => {
13
+ answer += text;
14
+ },
15
+ onDone: () => resolve({ answer, statuses }),
16
+ onError: () => resolve({ answer, statuses }),
17
+ });
18
+ });
19
+ }
20
+ describe('EmberSessionRegistry', () => {
21
+ it('reuses one transport handle across consecutive questions', async () => {
22
+ const transport = new StubEmberSessionTransportGateway();
23
+ let clock = new Date('2026-05-28T10:00:00Z');
24
+ const registry = new EmberSessionRegistry({
25
+ transport,
26
+ now: () => clock,
27
+ idleTimeoutMs: 60_000,
28
+ });
29
+ const first = registry.ask({ question: 'q1', systemPrompt: SYSTEM_PROMPT, projectPath: PROJECT_PATH });
30
+ if (first.status === 'streaming') {
31
+ await drain(first.subscribe);
32
+ }
33
+ clock = new Date('2026-05-28T10:00:10Z');
34
+ const second = registry.ask({ question: 'q2', systemPrompt: SYSTEM_PROMPT, projectPath: PROJECT_PATH });
35
+ if (second.status === 'streaming') {
36
+ await drain(second.subscribe);
37
+ }
38
+ expect(transport.spawnCount).toBe(1);
39
+ });
40
+ it('emits a working then idle status sequence for one question', async () => {
41
+ const transport = new StubEmberSessionTransportGateway();
42
+ const registry = new EmberSessionRegistry({
43
+ transport,
44
+ now: () => new Date('2026-05-28T10:00:00Z'),
45
+ idleTimeoutMs: 60_000,
46
+ });
47
+ const result = registry.ask({ question: 'q', systemPrompt: SYSTEM_PROMPT, projectPath: PROJECT_PATH });
48
+ expect(result.status).toBe('streaming');
49
+ if (result.status !== 'streaming') {
50
+ return;
51
+ }
52
+ const { statuses } = await drain(result.subscribe);
53
+ expect(statuses[0]).toBe('working');
54
+ expect(statuses.at(-1)).toBe('idle');
55
+ });
56
+ it('transparently revives with a fresh spawn after the idle timeout releases the session', async () => {
57
+ const transport = new StubEmberSessionTransportGateway();
58
+ let clock = new Date('2026-05-28T10:00:00Z');
59
+ const registry = new EmberSessionRegistry({
60
+ transport,
61
+ now: () => clock,
62
+ idleTimeoutMs: 60_000,
63
+ });
64
+ const first = registry.ask({ question: 'q1', systemPrompt: SYSTEM_PROMPT, projectPath: PROJECT_PATH });
65
+ if (first.status === 'streaming') {
66
+ await drain(first.subscribe);
67
+ }
68
+ clock = new Date('2026-05-28T10:05:00Z');
69
+ registry.onIdle(clock);
70
+ const second = registry.ask({ question: 'q2', systemPrompt: SYSTEM_PROMPT, projectPath: PROJECT_PATH });
71
+ if (second.status === 'streaming') {
72
+ await drain(second.subscribe);
73
+ }
74
+ expect(transport.spawnCount).toBe(2);
75
+ });
76
+ it('returns unavailable when the transport fails to spawn', () => {
77
+ const transport = new StubEmberSessionTransportGateway();
78
+ transport.failSpawn();
79
+ const registry = new EmberSessionRegistry({
80
+ transport,
81
+ now: () => new Date('2026-05-28T10:00:00Z'),
82
+ idleTimeoutMs: 60_000,
83
+ });
84
+ const result = registry.ask({ question: 'q', systemPrompt: SYSTEM_PROMPT, projectPath: PROJECT_PATH });
85
+ expect(result.status).toBe('unavailable');
86
+ });
87
+ });
88
+ //# sourceMappingURL=emberSessionRegistry.test.js.map