@molecule/app-ide-react 1.13.0 → 1.14.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 (36) hide show
  1. package/README.md +504 -1
  2. package/dist/command-metadata.d.ts.map +1 -1
  3. package/dist/command-metadata.js +7 -2
  4. package/dist/command-metadata.js.map +1 -1
  5. package/dist/components/ChatPanel.d.ts +9 -1
  6. package/dist/components/ChatPanel.d.ts.map +1 -1
  7. package/dist/components/ChatPanel.js +142 -18
  8. package/dist/components/ChatPanel.js.map +1 -1
  9. package/dist/components/TestsBar.d.ts +55 -0
  10. package/dist/components/TestsBar.d.ts.map +1 -0
  11. package/dist/components/TestsBar.js +296 -0
  12. package/dist/components/TestsBar.js.map +1 -0
  13. package/dist/components/TestsCard.d.ts +67 -0
  14. package/dist/components/TestsCard.d.ts.map +1 -0
  15. package/dist/components/TestsCard.js +185 -0
  16. package/dist/components/TestsCard.js.map +1 -0
  17. package/dist/components/index.d.ts +3 -0
  18. package/dist/components/index.d.ts.map +1 -1
  19. package/dist/components/index.js +2 -0
  20. package/dist/components/index.js.map +1 -1
  21. package/dist/components/tests-bar-utilities.d.ts +129 -0
  22. package/dist/components/tests-bar-utilities.d.ts.map +1 -0
  23. package/dist/components/tests-bar-utilities.js +236 -0
  24. package/dist/components/tests-bar-utilities.js.map +1 -0
  25. package/dist/components/tests-card-utilities.d.ts +145 -0
  26. package/dist/components/tests-card-utilities.d.ts.map +1 -0
  27. package/dist/components/tests-card-utilities.js +262 -0
  28. package/dist/components/tests-card-utilities.js.map +1 -0
  29. package/dist/index.d.ts +19 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +19 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/types.d.ts +136 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/package.json +2 -2
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Pure state + grouping helpers for the {@link TestsCard}.
3
+ *
4
+ * Everything here is a plain function over plain data, so the card's behaviour
5
+ * (which rows a group runs, how a streamed event moves the run forward, what
6
+ * the collapsed summary says) is unit-testable without rendering anything.
7
+ *
8
+ * @module
9
+ */
10
+ import type { TestItem, TestKind, TestRunEvent, TestRunOutcome, TestStatus, TestWorkspace } from '../types.js';
11
+ /** One rendered group of rows: a kind within a project directory. */
12
+ export interface TestGroup {
13
+ kind: TestKind;
14
+ workspace: TestWorkspace;
15
+ /** The directory's name for the heading; `null` for the workspace root. */
16
+ label: string | null;
17
+ items: TestItem[];
18
+ }
19
+ /** What one test file ended as in the last run. */
20
+ export interface TestResultEntry {
21
+ status: TestStatus;
22
+ durationMs?: number;
23
+ passed: number;
24
+ failed: number;
25
+ skipped: number;
26
+ /** The runner's output, kept for a FAILURE so the row can keep showing it. */
27
+ output?: string;
28
+ }
29
+ /** Everything the card knows about the run it is showing. */
30
+ export interface TestsRunState {
31
+ runId: string | null;
32
+ running: boolean;
33
+ /** The ids this run covers, in run order. */
34
+ queued: string[];
35
+ /** The id whose output is streaming right now, when the host says which. */
36
+ currentId: string | null;
37
+ /** The live output lines, newest last, capped at {@link MAX_OUTPUT_LINES}. */
38
+ output: string[];
39
+ /** Per-id outcome from this run (and from earlier runs, until re-run). */
40
+ results: Record<string, TestResultEntry>;
41
+ /** How the run ended, once it has. */
42
+ outcome: TestRunOutcome | null;
43
+ /** A run-level failure message (timeout, transport error) shown in the card. */
44
+ error: string | null;
45
+ startedAt: number | null;
46
+ durationMs: number | null;
47
+ }
48
+ /** Live output lines retained — enough to read, bounded so a chatty run cannot grow forever. */
49
+ export declare const MAX_OUTPUT_LINES = 400;
50
+ /** A run that has not started. */
51
+ export declare const EMPTY_RUN_STATE: TestsRunState;
52
+ /**
53
+ * Group tests for display: by kind (end-to-end first), then by project
54
+ * directory (whatever directories are present — `app`, `my-app/app`,
55
+ * `packages/web`, the root last), with the files sorted inside each group.
56
+ *
57
+ * @param tests - Every discovered test.
58
+ * @returns The groups, in display order. Empty groups are never produced.
59
+ */
60
+ export declare function groupTests(tests: readonly TestItem[]): TestGroup[];
61
+ /**
62
+ * How many tests there are of each kind.
63
+ *
64
+ * @param tests - Every discovered test.
65
+ * @returns The per-kind counts.
66
+ */
67
+ export declare function countByKind(tests: readonly TestItem[]): Record<TestKind, number>;
68
+ /**
69
+ * The tallies of the last run, over the tests that are still listed. Counted
70
+ * per FILE (one row, one verdict) so the collapsed summary matches the rows.
71
+ *
72
+ * @param tests - The currently listed tests.
73
+ * @param results - The per-id outcomes.
74
+ * @returns Passed/failed/skipped file counts.
75
+ */
76
+ export declare function summarizeResults(tests: readonly TestItem[], results: Record<string, TestResultEntry>): {
77
+ passed: number;
78
+ failed: number;
79
+ skipped: number;
80
+ reported: number;
81
+ };
82
+ /**
83
+ * Fold one streamed event into the run state.
84
+ *
85
+ * Deliberately total: an event for an id the card no longer lists is recorded
86
+ * anyway (a re-list may be in flight), and an unknown event type leaves the
87
+ * state untouched rather than throwing inside a stream handler.
88
+ *
89
+ * @param state - The current state.
90
+ * @param event - The event just received.
91
+ * @returns The next state (a new object whenever anything changed).
92
+ */
93
+ export declare function applyTestRunEvent(state: TestsRunState, event: TestRunEvent): TestsRunState;
94
+ /**
95
+ * The state after a run that never produced a `done` event — the host's stream
96
+ * died, or its request failed before the server could answer.
97
+ *
98
+ * @param state - The current state.
99
+ * @param message - What to show in the card.
100
+ * @returns The next state, no longer running.
101
+ */
102
+ export declare function failRun(state: TestsRunState, message: string): TestsRunState;
103
+ /**
104
+ * Whether a row should render as "running": the run is live and either the host
105
+ * named this row as current, or it is in the queue and has no verdict yet.
106
+ *
107
+ * @param state - The run state.
108
+ * @param id - The row's test id.
109
+ * @returns True when the row is part of the live run and still undecided.
110
+ */
111
+ export declare function isRowRunning(state: TestsRunState, id: string): boolean;
112
+ /**
113
+ * The label for a row: the host's title when it gave one, else the file path.
114
+ *
115
+ * @param item - The test.
116
+ * @returns The label to render.
117
+ */
118
+ export declare function testRowLabel(item: TestItem): string;
119
+ /**
120
+ * Filters tests by a free-text query, matching (case-insensitively) against the
121
+ * file path, the row title, and the project directory. A blank query returns
122
+ * every test in input order — the same contract as `filterScripts`.
123
+ *
124
+ * @param tests - The tests to filter.
125
+ * @param query - The search query.
126
+ * @returns The matching tests, in input order.
127
+ */
128
+ export declare function filterTests(tests: readonly TestItem[], query: string): TestItem[];
129
+ /**
130
+ * Parses a `/test [query | all]` command (`/tests` is the registered alias).
131
+ *
132
+ * `all` is the one argument that ACTS: it runs everything immediately. Every
133
+ * other argument only filters the list, exactly the way `/scripts <query>` seeds
134
+ * the scripts browser — running ONE test by name is what the per-row Run button
135
+ * is for, and giving the argument a second meaning would make `/test <thing>`
136
+ * sometimes list and sometimes execute.
137
+ *
138
+ * @param input - The raw chat input.
139
+ * @returns `{ query, runAll }` when it is a `/test` command, else `null`.
140
+ */
141
+ export declare function parseTestCommand(input: string): {
142
+ query: string;
143
+ runAll: boolean;
144
+ } | null;
145
+ //# sourceMappingURL=tests-card-utilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tests-card-utilities.d.ts","sourceRoot":"","sources":["../../src/components/tests-card-utilities.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Pure state + grouping helpers for the {@link TestsCard}.
3
+ *
4
+ * Everything here is a plain function over plain data, so the card's behaviour
5
+ * (which rows a group runs, how a streamed event moves the run forward, what
6
+ * the collapsed summary says) is unit-testable without rendering anything.
7
+ *
8
+ * @module
9
+ */
10
+ /** Live output lines retained — enough to read, bounded so a chatty run cannot grow forever. */
11
+ export const MAX_OUTPUT_LINES = 400;
12
+ /** Display order of the kinds: the preview-driven specs lead, unit tests follow. */
13
+ const KIND_ORDER = ['e2e', 'unit'];
14
+ /**
15
+ * Display order of the project directories within a kind: by path, with the
16
+ * workspace root (`.`) last — it is the least specific place a test can live.
17
+ *
18
+ * @param a - One directory.
19
+ * @param b - The other.
20
+ * @returns The sort order.
21
+ */
22
+ function compareWorkspaces(a, b) {
23
+ if (a === b)
24
+ return 0;
25
+ if (a === '.')
26
+ return 1;
27
+ if (b === '.')
28
+ return -1;
29
+ return a.localeCompare(b);
30
+ }
31
+ /**
32
+ * The heading name for a directory: the host's label when it sent one, else
33
+ * the path itself, and `null` for the root either way.
34
+ *
35
+ * @param item - Any test in the group.
36
+ * @returns The label, or `null` for the workspace root.
37
+ */
38
+ function labelFor(item) {
39
+ if (item.workspaceLabel !== undefined)
40
+ return item.workspaceLabel;
41
+ return item.workspace === '.' ? null : item.workspace;
42
+ }
43
+ /** A run that has not started. */
44
+ export const EMPTY_RUN_STATE = {
45
+ runId: null,
46
+ running: false,
47
+ queued: [],
48
+ currentId: null,
49
+ output: [],
50
+ results: {},
51
+ outcome: null,
52
+ error: null,
53
+ startedAt: null,
54
+ durationMs: null,
55
+ };
56
+ /**
57
+ * Group tests for display: by kind (end-to-end first), then by project
58
+ * directory (whatever directories are present — `app`, `my-app/app`,
59
+ * `packages/web`, the root last), with the files sorted inside each group.
60
+ *
61
+ * @param tests - Every discovered test.
62
+ * @returns The groups, in display order. Empty groups are never produced.
63
+ */
64
+ export function groupTests(tests) {
65
+ const groups = [];
66
+ for (const kind of KIND_ORDER) {
67
+ const workspaces = [
68
+ ...new Set(tests.filter((t) => t.kind === kind).map((t) => t.workspace)),
69
+ ].sort(compareWorkspaces);
70
+ for (const workspace of workspaces) {
71
+ const items = tests
72
+ .filter((t) => t.kind === kind && t.workspace === workspace)
73
+ .slice()
74
+ .sort((a, b) => a.file.localeCompare(b.file));
75
+ if (items.length > 0)
76
+ groups.push({ kind, workspace, label: labelFor(items[0]), items });
77
+ }
78
+ }
79
+ return groups;
80
+ }
81
+ /**
82
+ * How many tests there are of each kind.
83
+ *
84
+ * @param tests - Every discovered test.
85
+ * @returns The per-kind counts.
86
+ */
87
+ export function countByKind(tests) {
88
+ return {
89
+ e2e: tests.filter((t) => t.kind === 'e2e').length,
90
+ unit: tests.filter((t) => t.kind === 'unit').length,
91
+ };
92
+ }
93
+ /**
94
+ * The tallies of the last run, over the tests that are still listed. Counted
95
+ * per FILE (one row, one verdict) so the collapsed summary matches the rows.
96
+ *
97
+ * @param tests - The currently listed tests.
98
+ * @param results - The per-id outcomes.
99
+ * @returns Passed/failed/skipped file counts.
100
+ */
101
+ export function summarizeResults(tests, results) {
102
+ let passed = 0;
103
+ let failed = 0;
104
+ let skipped = 0;
105
+ for (const test of tests) {
106
+ const entry = results[test.id];
107
+ if (!entry)
108
+ continue;
109
+ if (entry.status === 'passed')
110
+ passed += 1;
111
+ else if (entry.status === 'failed')
112
+ failed += 1;
113
+ else
114
+ skipped += 1;
115
+ }
116
+ return { passed, failed, skipped, reported: passed + failed + skipped };
117
+ }
118
+ /**
119
+ * Fold one streamed event into the run state.
120
+ *
121
+ * Deliberately total: an event for an id the card no longer lists is recorded
122
+ * anyway (a re-list may be in flight), and an unknown event type leaves the
123
+ * state untouched rather than throwing inside a stream handler.
124
+ *
125
+ * @param state - The current state.
126
+ * @param event - The event just received.
127
+ * @returns The next state (a new object whenever anything changed).
128
+ */
129
+ export function applyTestRunEvent(state, event) {
130
+ switch (event.type) {
131
+ case 'start': {
132
+ // A new run clears the previous run's verdicts for the ids it covers, so
133
+ // a stale green pill never sits next to a row that is running again.
134
+ const results = { ...state.results };
135
+ for (const id of event.ids)
136
+ delete results[id];
137
+ return {
138
+ ...state,
139
+ runId: event.runId,
140
+ running: true,
141
+ queued: [...event.ids],
142
+ currentId: null,
143
+ output: [],
144
+ results,
145
+ outcome: null,
146
+ error: null,
147
+ startedAt: Date.now(),
148
+ durationMs: null,
149
+ };
150
+ }
151
+ case 'output': {
152
+ const output = [...state.output, ...event.chunk.split('\n')];
153
+ return {
154
+ ...state,
155
+ currentId: event.id ?? state.currentId,
156
+ output: output.length > MAX_OUTPUT_LINES ? output.slice(-MAX_OUTPUT_LINES) : output,
157
+ };
158
+ }
159
+ case 'result': {
160
+ return {
161
+ ...state,
162
+ results: {
163
+ ...state.results,
164
+ [event.id]: {
165
+ status: event.status,
166
+ ...(event.durationMs != null ? { durationMs: event.durationMs } : {}),
167
+ passed: event.passed ?? 0,
168
+ failed: event.failed ?? 0,
169
+ skipped: event.skipped ?? 0,
170
+ ...(event.output ? { output: event.output } : {}),
171
+ },
172
+ },
173
+ };
174
+ }
175
+ case 'done': {
176
+ return {
177
+ ...state,
178
+ running: false,
179
+ currentId: null,
180
+ outcome: event.outcome,
181
+ error: event.error ?? null,
182
+ durationMs: event.durationMs ?? (state.startedAt ? Date.now() - state.startedAt : null),
183
+ };
184
+ }
185
+ default:
186
+ return state;
187
+ }
188
+ }
189
+ /**
190
+ * The state after a run that never produced a `done` event — the host's stream
191
+ * died, or its request failed before the server could answer.
192
+ *
193
+ * @param state - The current state.
194
+ * @param message - What to show in the card.
195
+ * @returns The next state, no longer running.
196
+ */
197
+ export function failRun(state, message) {
198
+ return { ...state, running: false, currentId: null, outcome: 'error', error: message };
199
+ }
200
+ /**
201
+ * Whether a row should render as "running": the run is live and either the host
202
+ * named this row as current, or it is in the queue and has no verdict yet.
203
+ *
204
+ * @param state - The run state.
205
+ * @param id - The row's test id.
206
+ * @returns True when the row is part of the live run and still undecided.
207
+ */
208
+ export function isRowRunning(state, id) {
209
+ if (!state.running)
210
+ return false;
211
+ if (state.currentId === id)
212
+ return true;
213
+ return state.queued.includes(id) && state.results[id] == null;
214
+ }
215
+ /**
216
+ * The label for a row: the host's title when it gave one, else the file path.
217
+ *
218
+ * @param item - The test.
219
+ * @returns The label to render.
220
+ */
221
+ export function testRowLabel(item) {
222
+ return item.title?.trim() || item.file;
223
+ }
224
+ /**
225
+ * Filters tests by a free-text query, matching (case-insensitively) against the
226
+ * file path, the row title, and the project directory. A blank query returns
227
+ * every test in input order — the same contract as `filterScripts`.
228
+ *
229
+ * @param tests - The tests to filter.
230
+ * @param query - The search query.
231
+ * @returns The matching tests, in input order.
232
+ */
233
+ export function filterTests(tests, query) {
234
+ const q = query.trim().toLowerCase();
235
+ if (!q)
236
+ return [...tests];
237
+ return tests.filter((t) => t.file.toLowerCase().includes(q) ||
238
+ (t.title ?? '').toLowerCase().includes(q) ||
239
+ String(t.workspace).toLowerCase().includes(q));
240
+ }
241
+ /**
242
+ * Parses a `/test [query | all]` command (`/tests` is the registered alias).
243
+ *
244
+ * `all` is the one argument that ACTS: it runs everything immediately. Every
245
+ * other argument only filters the list, exactly the way `/scripts <query>` seeds
246
+ * the scripts browser — running ONE test by name is what the per-row Run button
247
+ * is for, and giving the argument a second meaning would make `/test <thing>`
248
+ * sometimes list and sometimes execute.
249
+ *
250
+ * @param input - The raw chat input.
251
+ * @returns `{ query, runAll }` when it is a `/test` command, else `null`.
252
+ */
253
+ export function parseTestCommand(input) {
254
+ const match = input.trim().match(/^\/tests?(?:\s+(.*))?$/i);
255
+ if (!match)
256
+ return null;
257
+ const argument = (match[1] ?? '').trim();
258
+ if (argument.toLowerCase() === 'all')
259
+ return { query: '', runAll: true };
260
+ return { query: argument, runAll: false };
261
+ }
262
+ //# sourceMappingURL=tests-card-utilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tests-card-utilities.js","sourceRoot":"","sources":["../../src/components/tests-card-utilities.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -84,6 +84,25 @@
84
84
  * the card steps aside while the banner is up — which only reads as one
85
85
  * consistent message if the banner's button is resolved from the same
86
86
  * `billingAction` the card used.
87
+ * - **`/test` lists and runs the project's tests** (`TestsCard`, opened by the
88
+ * `/test` command — `/tests` is a registered alias — in the same closeable
89
+ * overlay as `/scripts` and `/skills`, and renderable in the timeline as the
90
+ * `tests` system card). It owns no routes: pass `listTests` and `runTests`
91
+ * and the command works; omit either and it has nothing to run.
92
+ * `runTests(selection, onEvent)` returns a handle whose `cancel()` must
93
+ * really stop the run, and it must deliver exactly one `done` event however
94
+ * the run ends — including when the request never opened — or the card spins
95
+ * forever. `canRunTests` (a viewer: false) and `testsAvailable` (the
96
+ * environment is up) each disable the run controls and state their own
97
+ * reason in the card. The RUN is owned by `ChatPanel`, not the card, so it
98
+ * keeps streaming while the overlay is closed. `/test all` runs everything;
99
+ * any other argument only filters the list.
100
+ * - **End-to-end specs are meant to run against the LIVE PREVIEW.** The host
101
+ * should drive them through `@molecule/app-e2e-preview` (what every
102
+ * `mlcl create` app already bonds in `e2e/bonds.ts`), not a browser binary —
103
+ * in a sandbox there is none. That bond drives the page the person is
104
+ * actually looking at, so **a preview must be open** somewhere or the driver
105
+ * waits and fails; the card says so next to the end-to-end group.
87
106
  * - Text routes through `t('ide.*')` — `@molecule/app-locales-ide` supplies
88
107
  * translations.
89
108
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAEH,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,mBAAmB,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AAEH,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,mBAAmB,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -84,6 +84,25 @@
84
84
  * the card steps aside while the banner is up — which only reads as one
85
85
  * consistent message if the banner's button is resolved from the same
86
86
  * `billingAction` the card used.
87
+ * - **`/test` lists and runs the project's tests** (`TestsCard`, opened by the
88
+ * `/test` command — `/tests` is a registered alias — in the same closeable
89
+ * overlay as `/scripts` and `/skills`, and renderable in the timeline as the
90
+ * `tests` system card). It owns no routes: pass `listTests` and `runTests`
91
+ * and the command works; omit either and it has nothing to run.
92
+ * `runTests(selection, onEvent)` returns a handle whose `cancel()` must
93
+ * really stop the run, and it must deliver exactly one `done` event however
94
+ * the run ends — including when the request never opened — or the card spins
95
+ * forever. `canRunTests` (a viewer: false) and `testsAvailable` (the
96
+ * environment is up) each disable the run controls and state their own
97
+ * reason in the card. The RUN is owned by `ChatPanel`, not the card, so it
98
+ * keeps streaming while the overlay is closed. `/test all` runs everything;
99
+ * any other argument only filters the list.
100
+ * - **End-to-end specs are meant to run against the LIVE PREVIEW.** The host
101
+ * should drive them through `@molecule/app-e2e-preview` (what every
102
+ * `mlcl create` app already bonds in `e2e/bonds.ts`), not a browser binary —
103
+ * in a sandbox there is none. That bond drives the page the person is
104
+ * actually looking at, so **a preview must be open** somewhere or the driver
105
+ * waits and fails; the card says so next to the end-to-end group.
87
106
  * - Text routes through `t('ide.*')` — `@molecule/app-locales-ide` supplies
88
107
  * translations.
89
108
  *
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AAEH,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AAEH,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA"}
package/dist/types.d.ts CHANGED
@@ -65,6 +65,103 @@ export interface ChatUserIdentity {
65
65
  /** The clicked user's avatar value (data-URI / URL), if any. */
66
66
  avatar?: string | null;
67
67
  }
68
+ /**
69
+ * Which project directory a test file belongs to: the directory that OWNS the
70
+ * file (the nearest ancestor holding the runner's config or a `package.json`),
71
+ * as a path relative to the workspace root — `app`, `api`, `my-app/app`,
72
+ * `packages/web` — or `.` for the workspace root itself. Nothing guarantees
73
+ * `app/` or `api/` at the root: the executor names its project directory, so
74
+ * the value is a path, not an enum.
75
+ */
76
+ export type TestWorkspace = 'app' | 'api' | '.' | (string & {});
77
+ /**
78
+ * What a test file is: an end-to-end spec driven against the LIVE PREVIEW (the
79
+ * `@molecule/app-e2e-preview` bond every scaffolded app carries), or a plain
80
+ * unit test run by the project's own runner.
81
+ */
82
+ export type TestKind = 'e2e' | 'unit';
83
+ /** One test file the host discovered in the project. */
84
+ export interface TestItem {
85
+ /** Stable id, unique across workspaces — used as the row key and to select by. */
86
+ id: string;
87
+ /** Path relative to its workspace, e.g. `e2e/home.spec.ts`. */
88
+ file: string;
89
+ kind: TestKind;
90
+ workspace: TestWorkspace;
91
+ /**
92
+ * The group's project name, ready to render: the same path as `workspace`,
93
+ * or `null` for the workspace root, which has no name of its own. Hosts that
94
+ * omit it get the path itself (and the translated "Project" for the root).
95
+ */
96
+ workspaceLabel?: string | null;
97
+ /** Human label for the row; the bar falls back to the file path without one. */
98
+ title?: string;
99
+ }
100
+ /** Which runner drives each kind in one workspace (`null` = none installed). */
101
+ export interface TestRunners {
102
+ e2e: string | null;
103
+ unit: string | null;
104
+ }
105
+ /** What {@link ChatPanelProps.listTests} resolves with. */
106
+ export interface TestList {
107
+ tests: TestItem[];
108
+ /**
109
+ * Per project directory (keyed like {@link TestItem.workspace}), the runners
110
+ * the host found. Only directories that hold a listed test appear.
111
+ */
112
+ runners: Record<string, TestRunners>;
113
+ }
114
+ /** What the bar asks the host to run. */
115
+ export interface TestSelection {
116
+ /** Specific {@link TestItem.id}s. Takes precedence over `kind`. */
117
+ ids?: string[];
118
+ /** Everything of this kind when no `ids` are given. */
119
+ kind?: TestKind | 'all';
120
+ }
121
+ /** How one test file ended. */
122
+ export type TestStatus = 'passed' | 'failed' | 'skipped';
123
+ /** How a whole run ended. */
124
+ export type TestRunOutcome = 'completed' | 'cancelled' | 'timeout' | 'error';
125
+ /**
126
+ * One event from a run in progress. The host streams these to the bar (over SSE
127
+ * in molecule.dev) in the order the run produces them: one `start`, then
128
+ * interleaved `output`/`result`, then exactly one `done`.
129
+ */
130
+ export type TestRunEvent = {
131
+ type: 'start';
132
+ runId: string;
133
+ ids: string[];
134
+ startedAt?: string;
135
+ } | {
136
+ type: 'output';
137
+ id?: string;
138
+ stream?: 'stdout' | 'stderr';
139
+ chunk: string;
140
+ } | {
141
+ type: 'result';
142
+ id: string;
143
+ status: TestStatus;
144
+ durationMs?: number;
145
+ passed?: number;
146
+ failed?: number;
147
+ skipped?: number;
148
+ /** The runner's output for a FAILURE, which the bar keeps visible. */
149
+ output?: string;
150
+ } | {
151
+ type: 'done';
152
+ runId?: string;
153
+ outcome: TestRunOutcome;
154
+ passed?: number;
155
+ failed?: number;
156
+ skipped?: number;
157
+ durationMs?: number;
158
+ error?: string;
159
+ };
160
+ /** Handle to a run in flight, so the bar can stop it. */
161
+ export interface TestRunHandle {
162
+ /** Stop the run — the host aborts its stream, which cancels the work. */
163
+ cancel(): void;
164
+ }
68
165
  /**
69
166
  * Props for the {@link ChatPanel} component — the IDE chat surface plus the
70
167
  * callbacks the host app uses to react to AI activity (file changes, boot, client
@@ -401,6 +498,45 @@ export interface ChatPanelProps {
401
498
  * modal — which POSTs to the project's own backend — is unaffected.
402
499
  */
403
500
  feedbackUrl?: string;
501
+ /**
502
+ * Lists the project's tests for the `/test` browser. Omit it (the default)
503
+ * and `/test` has nothing to show — the shared IDE owns no test-discovery
504
+ * route of its own.
505
+ *
506
+ * molecule.dev implements it over `GET /projects/:id/tests`. It is called
507
+ * on every `/test` invocation and once more when a run finishes, so a spec
508
+ * the agent just wrote shows up the next time the browser is opened.
509
+ */
510
+ listTests?: () => Promise<TestList>;
511
+ /**
512
+ * Runs the selected tests, streaming {@link TestRunEvent}s back as they
513
+ * happen. Required alongside {@link ChatPanelProps.listTests} for the
514
+ * browser's run controls to work.
515
+ *
516
+ * The returned handle's `cancel()` must stop the run (molecule.dev aborts the
517
+ * SSE request, and the server kills the process tree on disconnect). The host
518
+ * is responsible for running the e2e specs through the preview bond chain —
519
+ * in a sandbox that means `npx playwright test` with
520
+ * `@molecule/app-e2e-preview` as the browser, so the spec drives the live
521
+ * preview rather than a browser binary that is not installed there.
522
+ *
523
+ * The run is owned by `ChatPanel`, not by the card, so it keeps streaming
524
+ * while the browser is closed and is still there when it is re-opened.
525
+ */
526
+ runTests?: (selection: TestSelection, onEvent: (event: TestRunEvent) => void) => TestRunHandle;
527
+ /**
528
+ * Whether this viewer may RUN tests. `false` still lets them open `/test`
529
+ * and read what the project tests (the platform serves the listing to
530
+ * viewers) but disables every run control and shows why. Defaults to
531
+ * `canEdit !== false`.
532
+ */
533
+ canRunTests?: boolean;
534
+ /**
535
+ * Whether the environment the tests run in is up — a running sandbox.
536
+ * `false` makes `/test` say to start the project instead of listing an empty
537
+ * browser or letting a Run click fail. Defaults to `true`.
538
+ */
539
+ testsAvailable?: boolean;
404
540
  className?: string;
405
541
  }
406
542
  /**