@plures/praxis 1.4.4 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -1067
- package/dist/browser/chunk-IUEKGHQN.js +373 -0
- package/dist/browser/factory/index.d.ts +2 -1
- package/dist/browser/index.d.ts +7 -4
- package/dist/browser/index.js +18 -6
- package/dist/browser/integrations/svelte.d.ts +4 -3
- package/dist/browser/project/index.d.ts +2 -1
- package/dist/browser/{reactive-engine.svelte-DgVTqHLc.d.ts → reactive-engine.svelte-BwWadvAW.d.ts} +2 -1
- package/dist/browser/rule-result-DcXWe9tn.d.ts +206 -0
- package/dist/browser/{rules-i1LHpnGd.d.ts → rules-BaWMqxuG.d.ts} +2 -205
- package/dist/browser/unified/index.d.ts +239 -0
- package/dist/browser/unified/index.js +20 -0
- package/dist/node/chunk-IUEKGHQN.js +373 -0
- package/dist/node/cli/index.js +1 -1
- package/dist/node/index.cjs +377 -0
- package/dist/node/index.d.cts +4 -2
- package/dist/node/index.d.ts +4 -2
- package/dist/node/index.js +19 -7
- package/dist/node/integrations/svelte.d.cts +3 -2
- package/dist/node/integrations/svelte.d.ts +3 -2
- package/dist/node/integrations/svelte.js +2 -2
- package/dist/node/{reactive-engine.svelte-DekxqFu0.d.ts → reactive-engine.svelte-BBZLMzus.d.ts} +3 -79
- package/dist/node/{reactive-engine.svelte-Cg0Yc2Hs.d.cts → reactive-engine.svelte-Cbq_V20o.d.cts} +3 -79
- package/dist/node/rule-result-B9GMivAn.d.cts +80 -0
- package/dist/node/rule-result-Bo3sFMmN.d.ts +80 -0
- package/dist/node/unified/index.cjs +494 -0
- package/dist/node/unified/index.d.cts +240 -0
- package/dist/node/unified/index.d.ts +240 -0
- package/dist/node/unified/index.js +21 -0
- package/docs/README.md +58 -102
- package/docs/archive/1.x/CONVERSATIONS_IMPLEMENTATION.md +207 -0
- package/docs/archive/1.x/DECISION_LEDGER_IMPLEMENTATION.md +109 -0
- package/docs/archive/1.x/DECISION_LEDGER_SUMMARY.md +424 -0
- package/docs/archive/1.x/ELEVATION_SUMMARY.md +249 -0
- package/docs/archive/1.x/FEATURE_SUMMARY.md +238 -0
- package/docs/archive/1.x/GOLDEN_PATH_IMPLEMENTATION.md +280 -0
- package/docs/archive/1.x/IMPLEMENTATION.md +166 -0
- package/docs/archive/1.x/IMPLEMENTATION_COMPLETE.md +389 -0
- package/docs/archive/1.x/IMPLEMENTATION_SUMMARY.md +59 -0
- package/docs/archive/1.x/INTEGRATION_ENHANCEMENT_SUMMARY.md +238 -0
- package/docs/archive/1.x/KNO_ENG_REFACTORING_SUMMARY.md +198 -0
- package/docs/archive/1.x/MONOREPO_SUMMARY.md +158 -0
- package/docs/archive/1.x/README.md +28 -0
- package/docs/archive/1.x/SVELTE_INTEGRATION_SUMMARY.md +415 -0
- package/docs/archive/1.x/TASK_1_COMPLETE.md +235 -0
- package/docs/archive/1.x/TASK_1_SUMMARY.md +281 -0
- package/docs/archive/1.x/VERSION_0.2.0_RELEASE_NOTES.md +288 -0
- package/docs/archive/1.x/ValidationChecklist.md +7 -0
- package/package.json +13 -1
- package/src/index.browser.ts +20 -0
- package/src/index.ts +21 -0
- package/src/unified/__tests__/unified-qa.test.ts +761 -0
- package/src/unified/__tests__/unified.test.ts +396 -0
- package/src/unified/core.ts +534 -0
- package/src/unified/index.ts +32 -0
- package/src/unified/rules.ts +66 -0
- package/src/unified/types.ts +148 -0
- package/dist/node/{chunk-ZO2LU4G4.js → chunk-WFRHXZBP.js} +3 -3
- package/dist/node/{validate-5PSWJTIC.js → validate-BY7JNY7H.js} +1 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis Unified Reactive Layer — Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests that the unified API works end-to-end:
|
|
5
|
+
* - Schema → query() → subscribe → reactive updates
|
|
6
|
+
* - mutate() → constraint check → rule evaluation → fact emission
|
|
7
|
+
* - Liveness detection
|
|
8
|
+
* - Batch mutations
|
|
9
|
+
* - Timeline logging
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
createApp,
|
|
15
|
+
definePath,
|
|
16
|
+
defineRule,
|
|
17
|
+
defineConstraint,
|
|
18
|
+
RuleResult,
|
|
19
|
+
fact,
|
|
20
|
+
} from '../index.js';
|
|
21
|
+
|
|
22
|
+
// ── Test Schema ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
interface SprintInfo {
|
|
25
|
+
name: string;
|
|
26
|
+
currentDay: number;
|
|
27
|
+
totalDays: number;
|
|
28
|
+
completedHours: number;
|
|
29
|
+
totalHours: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const Sprint = definePath<SprintInfo | null>('sprint/current', null);
|
|
33
|
+
const Loading = definePath<boolean>('sprint/loading', false);
|
|
34
|
+
const Items = definePath<Array<{ id: number; state: string; completedWork: number }>>('sprint/items', []);
|
|
35
|
+
|
|
36
|
+
// ── Test Rules ──────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const sprintBehindRule = defineRule({
|
|
39
|
+
id: 'sprint.behind',
|
|
40
|
+
watch: ['sprint/current'],
|
|
41
|
+
evaluate: (values) => {
|
|
42
|
+
const sprint = values['sprint/current'] as SprintInfo | null;
|
|
43
|
+
if (!sprint) return RuleResult.skip('No sprint');
|
|
44
|
+
const pace = sprint.currentDay / sprint.totalDays;
|
|
45
|
+
const work = sprint.completedHours / sprint.totalHours;
|
|
46
|
+
if (work >= pace) return RuleResult.retract(['sprint.behind']);
|
|
47
|
+
return RuleResult.emit([fact('sprint.behind', { pace, work })]);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const loadingRule = defineRule({
|
|
52
|
+
id: 'sprint.loading-check',
|
|
53
|
+
watch: ['sprint/loading', 'sprint/current'],
|
|
54
|
+
evaluate: (values) => {
|
|
55
|
+
const loading = values['sprint/loading'] as boolean;
|
|
56
|
+
const sprint = values['sprint/current'];
|
|
57
|
+
if (loading && !sprint) {
|
|
58
|
+
return RuleResult.emit([fact('sprint.still-loading', { since: Date.now() })]);
|
|
59
|
+
}
|
|
60
|
+
return RuleResult.retract(['sprint.still-loading']);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ── Test Constraints ────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const noCloseWithoutHours = defineConstraint({
|
|
67
|
+
id: 'no-close-without-hours',
|
|
68
|
+
description: 'Cannot have closed items with 0 completed hours',
|
|
69
|
+
watch: ['sprint/items'],
|
|
70
|
+
validate: (values) => {
|
|
71
|
+
const items = (values['sprint/items'] ?? []) as Array<{ id: number; state: string; completedWork: number }>;
|
|
72
|
+
const bad = items.find(i => i.state === 'Closed' && !i.completedWork);
|
|
73
|
+
if (bad) return `Item #${bad.id} cannot be closed with 0 completed hours`;
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe('Unified Reactive Layer', () => {
|
|
81
|
+
describe('createApp', () => {
|
|
82
|
+
it('creates an app with schema', () => {
|
|
83
|
+
const app = createApp({
|
|
84
|
+
name: 'test',
|
|
85
|
+
schema: [Sprint, Loading, Items],
|
|
86
|
+
});
|
|
87
|
+
expect(app).toBeDefined();
|
|
88
|
+
expect(app.query).toBeTypeOf('function');
|
|
89
|
+
expect(app.mutate).toBeTypeOf('function');
|
|
90
|
+
app.destroy();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('query()', () => {
|
|
95
|
+
it('returns initial value from schema', () => {
|
|
96
|
+
const app = createApp({ name: 'test', schema: [Sprint, Loading] });
|
|
97
|
+
const sprint = app.query<SprintInfo | null>('sprint/current');
|
|
98
|
+
expect(sprint.current).toBeNull();
|
|
99
|
+
app.destroy();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('is Svelte store compatible (subscribe with immediate callback)', () => {
|
|
103
|
+
const app = createApp({ name: 'test', schema: [Loading] });
|
|
104
|
+
const loading = app.query<boolean>('sprint/loading');
|
|
105
|
+
const values: boolean[] = [];
|
|
106
|
+
const unsub = loading.subscribe(v => values.push(v));
|
|
107
|
+
// Immediate callback on subscribe
|
|
108
|
+
expect(values).toEqual([false]);
|
|
109
|
+
unsub();
|
|
110
|
+
app.destroy();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('updates reactively on mutate', () => {
|
|
114
|
+
const app = createApp({ name: 'test', schema: [Sprint, Loading] });
|
|
115
|
+
const sprint = app.query<SprintInfo | null>('sprint/current');
|
|
116
|
+
const values: Array<SprintInfo | null> = [];
|
|
117
|
+
const unsub = sprint.subscribe(v => values.push(v));
|
|
118
|
+
|
|
119
|
+
// Initial
|
|
120
|
+
expect(values.length).toBe(1);
|
|
121
|
+
expect(values[0]).toBeNull();
|
|
122
|
+
|
|
123
|
+
// Mutate
|
|
124
|
+
const data: SprintInfo = { name: 'Sprint 1', currentDay: 3, totalDays: 10, completedHours: 5, totalHours: 20 };
|
|
125
|
+
app.mutate('sprint/current', data);
|
|
126
|
+
|
|
127
|
+
expect(values.length).toBe(2);
|
|
128
|
+
expect(values[1]).toEqual(data);
|
|
129
|
+
expect(sprint.current).toEqual(data);
|
|
130
|
+
|
|
131
|
+
unsub();
|
|
132
|
+
app.destroy();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('supports query options (where, sort, limit)', () => {
|
|
136
|
+
const app = createApp({ name: 'test', schema: [Items] });
|
|
137
|
+
const activeItems = app.query<Array<{ id: number; state: string; completedWork: number }>>('sprint/items', {
|
|
138
|
+
where: (item) => item.state === 'Active',
|
|
139
|
+
sort: (a, b) => a.id - b.id,
|
|
140
|
+
limit: 2,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
app.mutate('sprint/items', [
|
|
144
|
+
{ id: 3, state: 'Active', completedWork: 1 },
|
|
145
|
+
{ id: 1, state: 'Active', completedWork: 0 },
|
|
146
|
+
{ id: 2, state: 'Closed', completedWork: 5 },
|
|
147
|
+
{ id: 4, state: 'Active', completedWork: 2 },
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
expect(activeItems.current).toEqual([
|
|
151
|
+
{ id: 1, state: 'Active', completedWork: 0 },
|
|
152
|
+
{ id: 3, state: 'Active', completedWork: 1 },
|
|
153
|
+
]);
|
|
154
|
+
app.destroy();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('mutate()', () => {
|
|
159
|
+
it('returns accepted: true when no constraints violated', () => {
|
|
160
|
+
const app = createApp({ name: 'test', schema: [Sprint] });
|
|
161
|
+
const result = app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
|
|
162
|
+
expect(result.accepted).toBe(true);
|
|
163
|
+
expect(result.violations).toEqual([]);
|
|
164
|
+
app.destroy();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('rejects mutation when constraint violated', () => {
|
|
168
|
+
const app = createApp({
|
|
169
|
+
name: 'test',
|
|
170
|
+
schema: [Items],
|
|
171
|
+
constraints: [noCloseWithoutHours],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const result = app.mutate('sprint/items', [
|
|
175
|
+
{ id: 1, state: 'Closed', completedWork: 0 },
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
expect(result.accepted).toBe(false);
|
|
179
|
+
expect(result.violations.length).toBe(1);
|
|
180
|
+
expect(result.violations[0].message).toContain('Item #1');
|
|
181
|
+
|
|
182
|
+
// Value should NOT have been written
|
|
183
|
+
const items = app.query('sprint/items');
|
|
184
|
+
expect(items.current).toEqual([]);
|
|
185
|
+
|
|
186
|
+
app.destroy();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('triggers rule evaluation', () => {
|
|
190
|
+
const app = createApp({
|
|
191
|
+
name: 'test',
|
|
192
|
+
schema: [Sprint],
|
|
193
|
+
rules: [sprintBehindRule],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Behind: day 5/10 but only 2/20 hours done
|
|
197
|
+
app.mutate('sprint/current', {
|
|
198
|
+
name: 'Sprint 1',
|
|
199
|
+
currentDay: 5,
|
|
200
|
+
totalDays: 10,
|
|
201
|
+
completedHours: 2,
|
|
202
|
+
totalHours: 20,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const facts = app.facts();
|
|
206
|
+
expect(facts.some(f => f.tag === 'sprint.behind')).toBe(true);
|
|
207
|
+
|
|
208
|
+
// On pace: day 5/10, 12/20 hours done
|
|
209
|
+
app.mutate('sprint/current', {
|
|
210
|
+
name: 'Sprint 1',
|
|
211
|
+
currentDay: 5,
|
|
212
|
+
totalDays: 10,
|
|
213
|
+
completedHours: 12,
|
|
214
|
+
totalHours: 20,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const factsAfter = app.facts();
|
|
218
|
+
expect(factsAfter.some(f => f.tag === 'sprint.behind')).toBe(false);
|
|
219
|
+
|
|
220
|
+
app.destroy();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('batch()', () => {
|
|
225
|
+
it('applies multiple mutations atomically', () => {
|
|
226
|
+
const app = createApp({
|
|
227
|
+
name: 'test',
|
|
228
|
+
schema: [Sprint, Loading],
|
|
229
|
+
rules: [loadingRule],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const values: Array<SprintInfo | null> = [];
|
|
233
|
+
const sprintRef = app.query<SprintInfo | null>('sprint/current');
|
|
234
|
+
sprintRef.subscribe(v => values.push(v));
|
|
235
|
+
|
|
236
|
+
const result = app.batch((m) => {
|
|
237
|
+
m('sprint/loading', true);
|
|
238
|
+
m('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(result.accepted).toBe(true);
|
|
242
|
+
// Sprint was set
|
|
243
|
+
expect(sprintRef.current).toEqual({ name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
|
|
244
|
+
|
|
245
|
+
app.destroy();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('rejects entire batch on constraint violation', () => {
|
|
249
|
+
const app = createApp({
|
|
250
|
+
name: 'test',
|
|
251
|
+
schema: [Sprint, Items],
|
|
252
|
+
constraints: [noCloseWithoutHours],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const result = app.batch((m) => {
|
|
256
|
+
m('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
|
|
257
|
+
m('sprint/items', [{ id: 1, state: 'Closed', completedWork: 0 }]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(result.accepted).toBe(false);
|
|
261
|
+
// Neither mutation should have been applied
|
|
262
|
+
expect(app.query('sprint/current').current).toBeNull();
|
|
263
|
+
|
|
264
|
+
app.destroy();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('timeline', () => {
|
|
269
|
+
it('records mutations', () => {
|
|
270
|
+
const app = createApp({ name: 'test', schema: [Loading] });
|
|
271
|
+
app.mutate('sprint/loading', true);
|
|
272
|
+
app.mutate('sprint/loading', false);
|
|
273
|
+
|
|
274
|
+
const tl = app.timeline();
|
|
275
|
+
const mutations = tl.filter(e => e.kind === 'mutation');
|
|
276
|
+
expect(mutations.length).toBe(2);
|
|
277
|
+
expect(mutations[0].path).toBe('sprint/loading');
|
|
278
|
+
expect(mutations[0].data.before).toBe(false);
|
|
279
|
+
expect(mutations[0].data.after).toBe(true);
|
|
280
|
+
|
|
281
|
+
app.destroy();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('records rule evaluations', () => {
|
|
285
|
+
const app = createApp({
|
|
286
|
+
name: 'test',
|
|
287
|
+
schema: [Sprint],
|
|
288
|
+
rules: [sprintBehindRule],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 5, totalDays: 10, completedHours: 2, totalHours: 20 });
|
|
292
|
+
|
|
293
|
+
const tl = app.timeline();
|
|
294
|
+
const ruleEvals = tl.filter(e => e.kind === 'rule-eval');
|
|
295
|
+
expect(ruleEvals.some(e => e.data.ruleId === 'sprint.behind')).toBe(true);
|
|
296
|
+
|
|
297
|
+
app.destroy();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('liveness', () => {
|
|
302
|
+
it('detects stale paths', async () => {
|
|
303
|
+
const staleCb = vi.fn();
|
|
304
|
+
|
|
305
|
+
const app = createApp({
|
|
306
|
+
name: 'test',
|
|
307
|
+
schema: [Sprint, Loading],
|
|
308
|
+
liveness: {
|
|
309
|
+
expect: ['sprint/current'],
|
|
310
|
+
timeoutMs: 50,
|
|
311
|
+
onStale: staleCb,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Don't mutate sprint/current — it should go stale
|
|
316
|
+
await new Promise(r => setTimeout(r, 100));
|
|
317
|
+
|
|
318
|
+
expect(staleCb).toHaveBeenCalledWith('sprint/current', expect.any(Number));
|
|
319
|
+
|
|
320
|
+
const status = app.liveness();
|
|
321
|
+
expect(status['sprint/current'].stale).toBe(true);
|
|
322
|
+
|
|
323
|
+
app.destroy();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('does NOT flag paths that were updated', async () => {
|
|
327
|
+
const staleCb = vi.fn();
|
|
328
|
+
|
|
329
|
+
const app = createApp({
|
|
330
|
+
name: 'test',
|
|
331
|
+
schema: [Sprint],
|
|
332
|
+
liveness: {
|
|
333
|
+
expect: ['sprint/current'],
|
|
334
|
+
timeoutMs: 100,
|
|
335
|
+
onStale: staleCb,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Update immediately
|
|
340
|
+
app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
|
|
341
|
+
|
|
342
|
+
await new Promise(r => setTimeout(r, 150));
|
|
343
|
+
|
|
344
|
+
// onStale should NOT have been called
|
|
345
|
+
expect(staleCb).not.toHaveBeenCalled();
|
|
346
|
+
|
|
347
|
+
app.destroy();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('multiple subscribers', () => {
|
|
352
|
+
it('notifies all subscribers independently', () => {
|
|
353
|
+
const app = createApp({ name: 'test', schema: [Loading] });
|
|
354
|
+
|
|
355
|
+
const values1: boolean[] = [];
|
|
356
|
+
const values2: boolean[] = [];
|
|
357
|
+
|
|
358
|
+
const ref1 = app.query<boolean>('sprint/loading');
|
|
359
|
+
const ref2 = app.query<boolean>('sprint/loading');
|
|
360
|
+
|
|
361
|
+
const unsub1 = ref1.subscribe(v => values1.push(v));
|
|
362
|
+
const unsub2 = ref2.subscribe(v => values2.push(v));
|
|
363
|
+
|
|
364
|
+
app.mutate('sprint/loading', true);
|
|
365
|
+
|
|
366
|
+
expect(values1).toEqual([false, true]);
|
|
367
|
+
expect(values2).toEqual([false, true]);
|
|
368
|
+
|
|
369
|
+
unsub1();
|
|
370
|
+
app.mutate('sprint/loading', false);
|
|
371
|
+
|
|
372
|
+
// Only ref2 should get the update
|
|
373
|
+
expect(values1).toEqual([false, true]);
|
|
374
|
+
expect(values2).toEqual([false, true, false]);
|
|
375
|
+
|
|
376
|
+
unsub2();
|
|
377
|
+
app.destroy();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('destroy', () => {
|
|
382
|
+
it('cleans up all state', () => {
|
|
383
|
+
const app = createApp({
|
|
384
|
+
name: 'test',
|
|
385
|
+
schema: [Sprint, Loading, Items],
|
|
386
|
+
rules: [sprintBehindRule],
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
app.mutate('sprint/current', { name: 'Sprint 1', currentDay: 1, totalDays: 10, completedHours: 0, totalHours: 20 });
|
|
390
|
+
app.destroy();
|
|
391
|
+
|
|
392
|
+
expect(app.facts()).toEqual([]);
|
|
393
|
+
expect(app.timeline()).toEqual([]);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|