@things-factory/headless-twin 10.0.7 → 10.0.9
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/dist-server/engine/attention-digest.d.ts +52 -0
- package/dist-server/engine/attention-digest.js +76 -0
- package/dist-server/engine/attention-digest.js.map +1 -0
- package/dist-server/engine/board-vocabulary.d.ts +17 -0
- package/dist-server/engine/board-vocabulary.js +63 -0
- package/dist-server/engine/board-vocabulary.js.map +1 -0
- package/dist-server/engine/command-routing.d.ts +33 -0
- package/dist-server/engine/command-routing.js +53 -0
- package/dist-server/engine/command-routing.js.map +1 -0
- package/dist-server/engine/entity-delta.d.ts +13 -6
- package/dist-server/engine/entity-delta.js +38 -12
- package/dist-server/engine/entity-delta.js.map +1 -1
- package/dist-server/engine/index.d.ts +6 -0
- package/dist-server/engine/index.js +10 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-baseline.d.ts +78 -0
- package/dist-server/engine/kpi-baseline.js +123 -0
- package/dist-server/engine/kpi-baseline.js.map +1 -0
- package/dist-server/engine/kpi-broadcast.d.ts +4 -0
- package/dist-server/engine/kpi-broadcast.js +16 -0
- package/dist-server/engine/kpi-broadcast.js.map +1 -0
- package/dist-server/engine/kpi-query.d.ts +31 -0
- package/dist-server/engine/kpi-query.js +50 -2
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-attentions.d.ts +1 -0
- package/dist-server/engine/live-attentions.js +7 -1
- package/dist-server/engine/live-attentions.js.map +1 -1
- package/dist-server/engine/runtime-key.d.ts +15 -0
- package/dist-server/engine/runtime-key.js +64 -0
- package/dist-server/engine/runtime-key.js.map +1 -0
- package/dist-server/engine/state-axes.d.ts +16 -0
- package/dist-server/engine/state-axes.js +54 -0
- package/dist-server/engine/state-axes.js.map +1 -0
- package/dist-server/engine/twin-engine.d.ts +92 -16
- package/dist-server/engine/twin-engine.js +335 -77
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/engine/twin-level.d.ts +23 -0
- package/dist-server/engine/twin-level.js +52 -0
- package/dist-server/engine/twin-level.js.map +1 -0
- package/dist-server/engine/warm-start.d.ts +58 -12
- package/dist-server/engine/warm-start.js +80 -9
- package/dist-server/engine/warm-start.js.map +1 -1
- package/dist-server/service/reference/discovery-result.d.ts +34 -0
- package/dist-server/service/reference/discovery-result.js +84 -0
- package/dist-server/service/reference/discovery-result.js.map +1 -0
- package/dist-server/service/reference/ingest-space.d.ts +30 -0
- package/dist-server/service/reference/ingest-space.js +63 -0
- package/dist-server/service/reference/ingest-space.js.map +1 -0
- package/dist-server/service/reference/knob-defaults.d.ts +20 -0
- package/dist-server/service/reference/knob-defaults.js +59 -0
- package/dist-server/service/reference/knob-defaults.js.map +1 -0
- package/dist-server/service/reference/reference-live.js +2 -2
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-master.d.ts +37 -2
- package/dist-server/service/reference/reference-master.js +55 -5
- package/dist-server/service/reference/reference-master.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +2 -2
- package/dist-server/service/reference/reference-resolver.js +71 -15
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
- package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
- package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
- package/dist-server/service/twin-control/twin-control-mutation.js +29 -10
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
- package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +4 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-journal/twin-journal-query.d.ts +6 -2
- package/dist-server/service/twin-journal/twin-journal-query.js +25 -7
- package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +8 -0
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +23 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js +1 -1
- package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
- package/dist-server/service/twin-space/twin-space-resolver.js +9 -0
- package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
- package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
- package/dist-server/service/twin-structure/twin-structure.js +2 -1
- package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
- package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
- package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/engine/attention-digest.ts +102 -0
- package/server/engine/board-vocabulary.ts +61 -0
- package/server/engine/command-routing.ts +67 -0
- package/server/engine/entity-delta.ts +33 -11
- package/server/engine/index.ts +10 -0
- package/server/engine/kpi-baseline.ts +202 -0
- package/server/engine/kpi-broadcast.ts +13 -0
- package/server/engine/kpi-query.ts +81 -3
- package/server/engine/live-attentions.ts +7 -2
- package/server/engine/runtime-key.ts +58 -0
- package/server/engine/state-axes.ts +55 -0
- package/server/engine/twin-engine.ts +350 -77
- package/server/engine/twin-level.ts +48 -0
- package/server/engine/warm-start.ts +130 -16
- package/server/service/reference/discovery-result.ts +95 -0
- package/server/service/reference/ingest-space.ts +70 -0
- package/server/service/reference/knob-defaults.ts +59 -0
- package/server/service/reference/reference-live.ts +2 -2
- package/server/service/reference/reference-master.ts +94 -8
- package/server/service/reference/reference-resolver.ts +80 -16
- package/server/service/twin-attention/twin-attention-query.ts +43 -6
- package/server/service/twin-control/twin-control-mutation.ts +31 -12
- package/server/service/twin-forecast/twin-forecast-query.ts +3 -2
- package/server/service/twin-instance/twin-instance.ts +6 -2
- package/server/service/twin-journal/twin-journal-query.ts +35 -5
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +18 -2
- package/server/service/twin-metrics/twin-metrics-query.ts +1 -1
- package/server/service/twin-space/twin-space-resolver.ts +10 -1
- package/server/service/twin-state/twin-state-subscription.ts +1 -1
- package/server/service/twin-structure/twin-structure.ts +4 -1
- package/server/service/twin-target/twin-target-resolver.ts +22 -4
- package/test/attention-digest.test.ts +135 -0
- package/test/board-vocabulary.test.ts +114 -0
- package/test/capability-mapping.test.ts +4 -4
- package/test/command-routing.test.ts +61 -0
- package/test/discovery-result.test.ts +75 -0
- package/test/entity-delta.test.ts +25 -25
- package/test/ingest-bench.test.ts +3 -3
- package/test/ingest-space.test.ts +50 -0
- package/test/knob-defaults.test.ts +72 -0
- package/test/kpi-baseline-db.test.ts +214 -0
- package/test/kpi-baseline.test.ts +196 -0
- package/test/kpi-query-bench.test.ts +128 -0
- package/test/live-mirror-parity.test.ts +35 -2
- package/test/master-to-twin.test.ts +7 -3
- package/test/mutation-gate.test.ts +108 -0
- package/test/oee-accumulator.test.ts +65 -1
- package/test/registry-key-guard.test.ts +80 -0
- package/test/runtime-key.test.ts +66 -0
- package/test/scale-twin-bench.test.ts +2 -2
- package/test/state-axes.test.ts +74 -0
- package/test/streamline-e2e.test.ts +2 -2
- package/test/structure-revision-db.test.ts +5 -4
- package/test/tenant-registry-db.test.ts +149 -0
- package/test/warm-start-seam.test.ts +140 -0
- package/test/warm-start.test.ts +138 -4
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
import { test } from 'node:test'
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
|
|
14
|
-
import { buildEntityDeltas, ITEMS_AGGREGATE_LIMIT,
|
|
14
|
+
import { buildEntityDeltas, ITEMS_AGGREGATE_LIMIT, itemsTag, ordersTag, personsTag, tasksTag } from '../server/engine/entity-delta.ts'
|
|
15
15
|
|
|
16
16
|
const byTag = (arr: any[], tag: string) => arr.find(x => x.tag === tag)
|
|
17
17
|
|
|
18
18
|
test('노드: 계약의 속성이 전부 나간다 — parentId(소속 구역)를 포함해', () => {
|
|
19
19
|
const d = buildEntityDeltas({
|
|
20
20
|
locations: [{ id: 'bin-1', occupancy: 3, capacity: 10, status: 'idle', type: 'storage', parentId: 'zone-a' }]
|
|
21
|
-
})
|
|
21
|
+
}, 't1')
|
|
22
22
|
assert.deepEqual(byTag(d, 'bin-1').data, {
|
|
23
23
|
entity: 'location',
|
|
24
24
|
occupancy: 3,
|
|
@@ -32,7 +32,7 @@ test('노드: 계약의 속성이 전부 나간다 — parentId(소속 구역)
|
|
|
32
32
|
test('표식은 entity, kind 는 도메인 것 — 무버 종류가 표식에 덮이지 않는다', () => {
|
|
33
33
|
const d = buildEntityDeltas({
|
|
34
34
|
equipment: [{ id: 'cutter-1', kind: 'cutter', location: 'cut', status: 'down', held: true }]
|
|
35
|
-
})
|
|
35
|
+
}, 't1')
|
|
36
36
|
const m = byTag(d, 'cutter-1').data
|
|
37
37
|
assert.equal(m.entity, 'equipment', '표식')
|
|
38
38
|
assert.equal(m.kind, 'cutter', '도메인 속성 — 예전엔 표식에 덮여 사라졌다')
|
|
@@ -50,7 +50,7 @@ test('무버: OEE 는 정수 퍼센트로 정형화하고 원시 누적은 빼
|
|
|
50
50
|
oee: { overall: 0.8234, availability: 0.9, performance: 0.95, quality: 0.96, goodCount: 12, scrapCount: 3, idleMs: 99999 }
|
|
51
51
|
}
|
|
52
52
|
]
|
|
53
|
-
})
|
|
53
|
+
}, 't1')
|
|
54
54
|
assert.deepEqual(byTag(d, 'cutter-1').data.oee, {
|
|
55
55
|
overall: 82,
|
|
56
56
|
availability: 90,
|
|
@@ -62,7 +62,7 @@ test('무버: OEE 는 정수 퍼센트로 정형화하고 원시 누적은 빼
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
test('무버: oee 미측정이면 실리지 않는다', () => {
|
|
65
|
-
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', status: 'idle' }] })
|
|
65
|
+
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', status: 'idle' }] }, 't1')
|
|
66
66
|
assert.equal(byTag(d, 'fk-1').data.oee, undefined)
|
|
67
67
|
})
|
|
68
68
|
|
|
@@ -72,7 +72,7 @@ test('작업: 개별 + 집약으로 나간다 — 예전엔 아예 방출되지
|
|
|
72
72
|
{ id: 't1', kind: 'putaway', status: 'in-progress', fromNode: 'dock', toNode: 'rack-1', resourceRef: 'fk-1', orderId: 'o1', progress: 0.4 },
|
|
73
73
|
{ id: 't2', kind: 'pick', status: 'created' }
|
|
74
74
|
]
|
|
75
|
-
})
|
|
75
|
+
}, 't1')
|
|
76
76
|
assert.deepEqual(byTag(d, 't1').data, {
|
|
77
77
|
entity: 'task',
|
|
78
78
|
kind: 'putaway',
|
|
@@ -83,7 +83,7 @@ test('작업: 개별 + 집약으로 나간다 — 예전엔 아예 방출되지
|
|
|
83
83
|
orderId: 'o1',
|
|
84
84
|
progress: 0.4
|
|
85
85
|
})
|
|
86
|
-
const agg = byTag(d,
|
|
86
|
+
const agg = byTag(d, tasksTag('t1')).data
|
|
87
87
|
assert.equal(agg.entity, 'tasks')
|
|
88
88
|
assert.equal(agg.tasks.length, 2)
|
|
89
89
|
assert.equal(agg.tasks[0].id, 't1', '집약에는 식별자가 있다')
|
|
@@ -93,7 +93,7 @@ test('작업: 개별 + 집약으로 나간다 — 예전엔 아예 방출되지
|
|
|
93
93
|
test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔레트·만료까지', () => {
|
|
94
94
|
const d = buildEntityDeltas({
|
|
95
95
|
items: [{ epc: 'urn:epc:id:sgtin:1.1.1', gtin: 'g1', location: 'rack-1', disposition: 'sellable', parent: 'sscc-1', qty: 40, expiry: 1234 }]
|
|
96
|
-
})
|
|
96
|
+
}, 't1')
|
|
97
97
|
assert.deepEqual(byTag(d, 'urn:epc:id:sgtin:1.1.1').data, {
|
|
98
98
|
entity: 'item',
|
|
99
99
|
gtin: 'g1',
|
|
@@ -103,7 +103,7 @@ test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔
|
|
|
103
103
|
qty: 40,
|
|
104
104
|
expiry: 1234
|
|
105
105
|
})
|
|
106
|
-
const agg = byTag(d,
|
|
106
|
+
const agg = byTag(d, itemsTag('t1')).data
|
|
107
107
|
assert.equal(agg.entity, 'items')
|
|
108
108
|
assert.equal(agg.items[0].epc, 'urn:epc:id:sgtin:1.1.1')
|
|
109
109
|
assert.equal(agg.items[0].qty, 40)
|
|
@@ -113,8 +113,8 @@ test('물품: 개별(태그=epc) + 집약으로 나간다 — 수량·소속 팔
|
|
|
113
113
|
|
|
114
114
|
test('물품 집약: 상한을 넘으면 잘린 사실을 밝힌다 — 조용한 절단 금지', () => {
|
|
115
115
|
const items = Array.from({ length: ITEMS_AGGREGATE_LIMIT + 7 }, (_, i) => ({ epc: `e${i}`, location: 'rack-1' }))
|
|
116
|
-
const d = buildEntityDeltas({ items })
|
|
117
|
-
const agg = byTag(d,
|
|
116
|
+
const d = buildEntityDeltas({ items }, 't1')
|
|
117
|
+
const agg = byTag(d, itemsTag('t1')).data
|
|
118
118
|
assert.equal(agg.items.length, ITEMS_AGGREGATE_LIMIT)
|
|
119
119
|
assert.equal(agg.total, ITEMS_AGGREGATE_LIMIT + 7)
|
|
120
120
|
assert.equal(agg.truncated, 7)
|
|
@@ -128,9 +128,9 @@ test('오더: 개별 + 집약, 도메인 kind 유지', () => {
|
|
|
128
128
|
{ id: 'o1', kind: 'workorder', status: 'picking', progress: 0.5, held: false },
|
|
129
129
|
{ id: 'o2', kind: 'outbound', status: 'created', progress: 0, held: true }
|
|
130
130
|
]
|
|
131
|
-
})
|
|
131
|
+
}, 't1')
|
|
132
132
|
assert.equal(byTag(d, 'o1').data.kind, 'workorder')
|
|
133
|
-
const agg = byTag(d,
|
|
133
|
+
const agg = byTag(d, ordersTag('t1')).data
|
|
134
134
|
assert.equal(agg.entity, 'orders')
|
|
135
135
|
assert.equal(agg.orders.length, 2)
|
|
136
136
|
assert.equal(agg.orders[1].held, true, '집약에 held 반영')
|
|
@@ -138,23 +138,23 @@ test('오더: 개별 + 집약, 도메인 kind 유지', () => {
|
|
|
138
138
|
|
|
139
139
|
test('계약에 새 속성이 생기면 손대지 않아도 도달한다 — 화이트리스트 재발 방지', () => {
|
|
140
140
|
/* 이 테스트가 실패하면 누군가 필드를 손으로 골라 담기 시작한 것이다. */
|
|
141
|
-
const d = buildEntityDeltas({ locations: [{ id: 'n1', occupancy: 0, temperatureC: -18, hazmatClass: '3' }] })
|
|
141
|
+
const d = buildEntityDeltas({ locations: [{ id: 'n1', occupancy: 0, temperatureC: -18, hazmatClass: '3' }] }, 't1')
|
|
142
142
|
const n = byTag(d, 'n1').data
|
|
143
143
|
assert.equal(n.temperatureC, -18, '알지 못하는 새 속성도 통과해야 한다')
|
|
144
144
|
assert.equal(n.hazmatClass, '3')
|
|
145
145
|
})
|
|
146
146
|
|
|
147
147
|
test('undefined 값은 싣지 않는다 — 키 유무가 시그니처를 흔들지 않게', () => {
|
|
148
|
-
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', location: undefined, status: 'idle' }] })
|
|
148
|
+
const d = buildEntityDeltas({ equipment: [{ id: 'fk-1', kind: 'forklift', location: undefined, status: 'idle' }] }, 't1')
|
|
149
149
|
assert.equal('location' in byTag(d, 'fk-1').data, false)
|
|
150
150
|
})
|
|
151
151
|
|
|
152
152
|
test('빈 상태 — 집약 태그 셋은 항상 방출(빈 배열)', () => {
|
|
153
|
-
const d = buildEntityDeltas({})
|
|
154
|
-
assert.deepEqual(byTag(d,
|
|
155
|
-
assert.deepEqual(byTag(d,
|
|
156
|
-
assert.deepEqual(byTag(d,
|
|
157
|
-
assert.equal(byTag(d,
|
|
153
|
+
const d = buildEntityDeltas({}, 't1')
|
|
154
|
+
assert.deepEqual(byTag(d, ordersTag('t1')).data.orders, [])
|
|
155
|
+
assert.deepEqual(byTag(d, tasksTag('t1')).data.tasks, [])
|
|
156
|
+
assert.deepEqual(byTag(d, itemsTag('t1')).data.items, [])
|
|
157
|
+
assert.equal(byTag(d, itemsTag('t1')).data.total, 0)
|
|
158
158
|
})
|
|
159
159
|
|
|
160
160
|
/* ── 사람 ────────────────────────────────────────────────────────────────
|
|
@@ -165,7 +165,7 @@ test('빈 상태 — 집약 태그 셋은 항상 방출(빈 배열)', () => {
|
|
|
165
165
|
test('사람이 개별 태그로 나간다 — 등급·교대가 그대로', () => {
|
|
166
166
|
const deltas = buildEntityDeltas({
|
|
167
167
|
persons: [{ id: 'w1', personnelClass: 'welder-op', status: 'busy', taskId: 't1', offShift: undefined }]
|
|
168
|
-
})
|
|
168
|
+
}, 't1')
|
|
169
169
|
const one = deltas.find(d => d.tag === 'w1')!
|
|
170
170
|
assert.equal(one.data.entity, 'person')
|
|
171
171
|
assert.equal(one.data.personnelClass, 'welder-op')
|
|
@@ -174,9 +174,9 @@ test('사람이 개별 태그로 나간다 — 등급·교대가 그대로', ()
|
|
|
174
174
|
})
|
|
175
175
|
|
|
176
176
|
test('사람 집약 태그가 항상 나간다 — 선언하지 않은 트윈은 빈 배열(없음과 모름을 구별)', () => {
|
|
177
|
-
const withNone = buildEntityDeltas({})
|
|
178
|
-
const agg = withNone.find(d => d.tag ===
|
|
177
|
+
const withNone = buildEntityDeltas({}, 't1')
|
|
178
|
+
const agg = withNone.find(d => d.tag === personsTag('t1'))!
|
|
179
179
|
assert.deepEqual(agg.data.persons, [])
|
|
180
|
-
const withTwo = buildEntityDeltas({ persons: [{ id: 'w1' }, { id: 'w2' }] })
|
|
181
|
-
assert.equal(withTwo.find(d => d.tag ===
|
|
180
|
+
const withTwo = buildEntityDeltas({ persons: [{ id: 'w1' }, { id: 'w2' }] }, 't1')
|
|
181
|
+
assert.equal(withTwo.find(d => d.tag === personsTag('t1'))!.data.persons.length, 2)
|
|
182
182
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* 커널 인제스트 인터페이스 부하 실측(가정 아닌 측정) — Face1/커널 동기 인터페이스의 처리량 특성.
|
|
3
|
-
* apply(이벤트 흡수) · snapshot(전체 투영) · buildEntityDeltas(방송 payload) 의 비용을 규모별로 잰다.
|
|
3
|
+
* apply(이벤트 흡수) · snapshot(전체 투영) · buildEntityDeltas(방송 payload, 't1') 의 비용을 규모별로 잰다.
|
|
4
4
|
* 결론은 design/integration/ingest-scale.md §커널 인터페이스에 스펙으로 반영.
|
|
5
5
|
* (순수 커널+호스트 매핑, shell 무관 → node:test.)
|
|
6
6
|
*/
|
|
@@ -50,7 +50,7 @@ test('bench: buildEntityDeltas 비용 vs 엔티티 수 (전 엔티티 매번 방
|
|
|
50
50
|
const snap = proj.snapshot()
|
|
51
51
|
const iters = 200
|
|
52
52
|
const t0 = performance.now()
|
|
53
|
-
for (let k = 0; k < iters; k++) buildEntityDeltas(snap)
|
|
53
|
+
for (let k = 0; k < iters; k++) buildEntityDeltas(snap, 't1')
|
|
54
54
|
const per = (performance.now() - t0) / iters
|
|
55
55
|
console.log(` [buildEntityDeltas] @ ${N.toLocaleString()} locations: ${per.toFixed(3)}ms/call`)
|
|
56
56
|
}
|
|
@@ -62,7 +62,7 @@ test('bench: 방송 1회 총비용(snapshot + buildEntityDeltas) vs 상태 규
|
|
|
62
62
|
for (let i = 0; i < items; i++) proj.apply(envelope(`urn:epc:id:sscc:0614141.${i}`, `bin-${i % locations}`))
|
|
63
63
|
const iters = 100
|
|
64
64
|
const t0 = performance.now()
|
|
65
|
-
for (let k = 0; k < iters; k++) buildEntityDeltas(proj.snapshot())
|
|
65
|
+
for (let k = 0; k < iters; k++) buildEntityDeltas(proj.snapshot(), 't1')
|
|
66
66
|
const per = (performance.now() - t0) / iters
|
|
67
67
|
console.log(` [방송1회] ${locations.toLocaleString()} locations / ${items.toLocaleString()} items: ${per.toFixed(3)}ms → 최대 ${Math.round(1000 / per).toLocaleString()} 방송/s`)
|
|
68
68
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 인제스트 공간 판정 — "이 현장에 더한다" 가 조용히 빗나가지 않게.
|
|
3
|
+
*
|
|
4
|
+
* 엔진 본체는 DB·TypeORM 을 물고 있어 단위테스트로 부를 수 없다. 그래서 **결정만** 순수 모듈로
|
|
5
|
+
* 떼어 놓고 여기서 본다(엔진은 공간 존재 여부를 조회해 넘기는 일만 한다).
|
|
6
|
+
*/
|
|
7
|
+
import { test } from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import { resolveIngestSpace, collidingIds, INGEST_SPACE_NOT_FOUND } from '../server/service/reference/ingest-space.ts'
|
|
11
|
+
|
|
12
|
+
test('고르지 않으면 마스터가 파생한 공간으로 — 지금까지의 거동을 바꾸지 않는다', () => {
|
|
13
|
+
for (const req of [undefined, null, '', ' ']) {
|
|
14
|
+
const c = resolveIngestSpace('hatiolab-wms-site', req as any, false)
|
|
15
|
+
assert.deepEqual(c, { spaceId: 'hatiolab-wms-site', joined: false }, `요청=${JSON.stringify(req)}`)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('기존 현장을 고르면 그 공간으로 들어가고 합치기로 표시된다', () => {
|
|
20
|
+
const c = resolveIngestSpace('trailer-yard-new-site', 'rosarito-site', true)
|
|
21
|
+
assert.deepEqual(c, { spaceId: 'rosarito-site', joined: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('마스터가 이미 그 공간이면 합치기가 아니다 — 같은 공간의 재인제스트다', () => {
|
|
25
|
+
const c = resolveIngestSpace('rosarito-site', 'rosarito-site', true)
|
|
26
|
+
assert.deepEqual(c, { spaceId: 'rosarito-site', joined: false })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('없는 현장은 거절한다 — 조용히 새 공간을 만들지 않는다', () => {
|
|
30
|
+
/* 여기서 새 공간을 만들어 주면 사용자는 합쳤다고 믿고 화면은 따로 논다. 그때는 이미 양쪽에
|
|
31
|
+
이벤트가 쌓여 있어 손으로 합치는 것이 훨씬 비싸다. */
|
|
32
|
+
assert.throws(
|
|
33
|
+
() => resolveIngestSpace('a-site', 'typo-site', false),
|
|
34
|
+
(e: any) => e.code === INGEST_SPACE_NOT_FOUND && /typo-site/.test(e.message)
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('앞뒤 공백은 다듬는다 — 사용자가 붙여 넣은 id 로 거절당하지 않게', () => {
|
|
39
|
+
const c = resolveIngestSpace('a-site', ' rosarito-site ', true)
|
|
40
|
+
assert.equal(c.spaceId, 'rosarito-site')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('같은 id 를 쓰는 것들을 찾아낸다 — 합쳐지는 것을 모르고 지나가지 않게', () => {
|
|
44
|
+
const existing = [{ id: 'a-yard' }, { id: 'a-dock' }]
|
|
45
|
+
const incoming = [{ id: 'a-yard' }, { id: 'a-line' }, { id: 'a-dock' }]
|
|
46
|
+
assert.deepEqual(collidingIds(existing, incoming), ['a-dock', 'a-yard'], '정렬된 교집합')
|
|
47
|
+
assert.deepEqual(collidingIds(existing, [{ id: 'a-line' }]), [], '겹치지 않으면 빈 배열')
|
|
48
|
+
assert.deepEqual(collidingIds(undefined, undefined), [], '없는 쪽은 빈 것으로 본다')
|
|
49
|
+
assert.deepEqual(collidingIds([{}, { id: '' }], [{ id: '' }]), [], 'id 없는 항목은 세지 않는다')
|
|
50
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 노브 기본값 — **조용히 아무것도 안 하는 트윈**이 만들어지지 않게.
|
|
3
|
+
*
|
|
4
|
+
* 기본값을 아는 곳은 노브 스키마(계약)인데 그것을 적용하는 곳이 위자드뿐이었다. 그래서 화면을 거치지
|
|
5
|
+
* 않는 호출자가 파라미터를 일부만 주면 템플릿 안에서 발생률이 0 이 되고, 이벤트를 하나도 만들지 않는
|
|
6
|
+
* 트윈이 아무 말 없이 생겼다(실제로 밟았고 템플릿이 깨진 것으로 오진했다).
|
|
7
|
+
*/
|
|
8
|
+
import { test } from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
|
|
11
|
+
import { applyKnobDefaults, filledKnobKeys } from '../server/service/reference/knob-defaults.ts'
|
|
12
|
+
|
|
13
|
+
const KNOBS = [
|
|
14
|
+
{ key: 'id', default: 'wms-new' },
|
|
15
|
+
{ key: 'inboundRate', default: 40 },
|
|
16
|
+
{ key: 'outboundRate', default: 30 },
|
|
17
|
+
{ key: 'geo', default: { lat: 37.5, lon: 127.0 } },
|
|
18
|
+
{ key: 'noDefault' } // 기본값을 선언하지 않은 노브
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
test('빠진 노브를 기본값으로 채운다 — 부분 파라미터 호출이 죽은 트윈을 만들지 않게', () => {
|
|
22
|
+
const out = applyKnobDefaults(KNOBS, { id: 'mine' })
|
|
23
|
+
assert.equal(out.id, 'mine', '준 값은 그대로')
|
|
24
|
+
assert.equal(out.inboundRate, 40)
|
|
25
|
+
assert.equal(out.outboundRate, 30)
|
|
26
|
+
assert.deepEqual(out.geo, { lat: 37.5, lon: 127.0 })
|
|
27
|
+
assert.ok(!('noDefault' in out), '기본값 없는 노브는 만들어 내지 않는다')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('0 은 덮지 않는다 — "자극 없이 구조만" 이라는 선언을 뒤집지 않게', () => {
|
|
31
|
+
/* falsy 로 판정해 덮으면 발생률 0(도착 없음)이 40 으로 바뀐다. 없는 것과 0 은 다르다. */
|
|
32
|
+
const out = applyKnobDefaults(KNOBS, { inboundRate: 0, outboundRate: 0 })
|
|
33
|
+
assert.equal(out.inboundRate, 0)
|
|
34
|
+
assert.equal(out.outboundRate, 0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('빈 문자열·false 도 뜻 있는 값이라 덮지 않는다', () => {
|
|
38
|
+
const out = applyKnobDefaults([{ key: 'name', default: 'New site' }, { key: 'flag', default: true }], { name: '', flag: false })
|
|
39
|
+
assert.equal(out.name, '')
|
|
40
|
+
assert.equal(out.flag, false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('null 은 "주지 않음" 으로 본다 — JSON·GraphQL 경로가 그렇게 보낸다', () => {
|
|
44
|
+
const out = applyKnobDefaults(KNOBS, { inboundRate: null as any })
|
|
45
|
+
assert.equal(out.inboundRate, 40)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('선언 밖 파라미터는 손대지 않는다 — 템플릿의 폴백 경로를 끊지 않게', () => {
|
|
49
|
+
/* 템플릿은 `geo` 대신 `lat`/`lon` 을 읽는 폴백을 가진다. 모르는 키를 걷어내면 그 경로가 끊긴다. */
|
|
50
|
+
const out = applyKnobDefaults(KNOBS, { lat: 1, lon: 2 })
|
|
51
|
+
assert.equal(out.lat, 1)
|
|
52
|
+
assert.equal(out.lon, 2)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('입력을 바꾸지 않는다 — 호출자의 객체가 몰래 변하면 추적이 어렵다', () => {
|
|
56
|
+
const given = { id: 'mine' }
|
|
57
|
+
applyKnobDefaults(KNOBS, given)
|
|
58
|
+
assert.deepEqual(given, { id: 'mine' })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('노브가 없거나 파라미터가 없어도 무너지지 않는다', () => {
|
|
62
|
+
assert.deepEqual(applyKnobDefaults(null, { a: 1 }), { a: 1 })
|
|
63
|
+
assert.deepEqual(applyKnobDefaults(KNOBS, null).inboundRate, 40)
|
|
64
|
+
assert.deepEqual(applyKnobDefaults(undefined, undefined), {})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('무엇을 채웠는지 알린다 — 준 적 없는 값으로 만들어진 것을 밝히게', () => {
|
|
68
|
+
assert.deepEqual(filledKnobKeys(KNOBS, { id: 'mine', inboundRate: 0 }), ['outboundRate', 'geo'],
|
|
69
|
+
'준 것과 0 은 채운 목록에 없다')
|
|
70
|
+
assert.deepEqual(filledKnobKeys(KNOBS, null), ['id', 'inboundRate', 'outboundRate', 'geo'])
|
|
71
|
+
assert.deepEqual(filledKnobKeys(KNOBS, { id: 'a', inboundRate: 1, outboundRate: 2, geo: {} }), [], '다 주면 빈 목록')
|
|
72
|
+
})
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ★ 여러 구간 동시 비교 — **실 DB로 배관을 넘어 본다.**
|
|
3
|
+
*
|
|
4
|
+
* 분포 산수는 `kpi-baseline.test.ts`(순수)가 지킨다. 여기서 지키는 것은 **호스트 배관**이다:
|
|
5
|
+
* 창을 하루씩 뒤로 옮겨 **각 창을 따로 읽는가** · 그 창을 같은 규칙으로 접는가 · 지표 값을 정본 표
|
|
6
|
+
* (`TWIN_METRIC`)로 꺼내는가 · 기록 없는 날을 결측으로 남기는가.
|
|
7
|
+
*
|
|
8
|
+
* 이 배관은 **경계를 넘어야만** 틀리는 것이 드러난다. 실제로 앱의 시뮬 트윈은 저널이 두 시간 남짓이라
|
|
9
|
+
* "지난 7일" 을 물어도 과거 창이 모두 비어 있어(정직하게 자리를 거부한다) 분포 경로를 한 번도 밟지
|
|
10
|
+
* 못했다. 그래서 여러 날에 걸친 저널을 손으로 심어 그 경로를 통과시킨다.
|
|
11
|
+
*
|
|
12
|
+
* 실 엔티티·실 네이밍 전략의 sqlite in-memory 스키마에 **빌드 산출물**을 실행한다.
|
|
13
|
+
* **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌려야 반영된다.**
|
|
14
|
+
*/
|
|
15
|
+
import { test, before, after } from 'node:test'
|
|
16
|
+
import assert from 'node:assert/strict'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import type { DataSource } from 'typeorm'
|
|
19
|
+
|
|
20
|
+
const req = createRequire(import.meta.url)
|
|
21
|
+
const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
|
|
22
|
+
const { entities: authEntities } = req('@things-factory/auth-base')
|
|
23
|
+
const { entities: twinEntities } = req('../dist-server/service/index.js')
|
|
24
|
+
const { computeTwinKpi } = req('../dist-server/engine/index.js')
|
|
25
|
+
const { TwinEvent } = req('../dist-server/service/twin-event/twin-event.js')
|
|
26
|
+
const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
|
|
27
|
+
const { DataSource: TypeOrmDataSource } = req('typeorm')
|
|
28
|
+
|
|
29
|
+
let ds: DataSource
|
|
30
|
+
let domainId: string
|
|
31
|
+
|
|
32
|
+
const INSTANCE = 'baseline-probe'
|
|
33
|
+
const MS_DAY = 24 * 60 * 60_000
|
|
34
|
+
|
|
35
|
+
/** 창의 끝 — 고정 시각을 쓴다(테스트가 벽시계에 흔들리지 않게). */
|
|
36
|
+
const TO = Date.parse('2026-03-10T10:00:00.000Z')
|
|
37
|
+
const FROM = TO - 60 * 60_000
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 완료 한 건 = 시작 + 완료 두 줄. 소요시간이 짝맞춤에서 나오므로 둘 다 필요하다.
|
|
41
|
+
* 완료 시각이 창 안에 들면 그 창의 성과로 센다.
|
|
42
|
+
*/
|
|
43
|
+
function taskRows(dayBack: number, count: number, workMs: number, revBase: number) {
|
|
44
|
+
const rows: any[] = []
|
|
45
|
+
const endBase = TO - dayBack * MS_DAY - 60_000 // 창 끝 1분 전에 완료
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
const taskId = `t-${dayBack}-${i}`
|
|
48
|
+
const done = endBase - i * 1000
|
|
49
|
+
const started = done - workMs
|
|
50
|
+
rows.push(
|
|
51
|
+
{
|
|
52
|
+
domain: { id: domainId },
|
|
53
|
+
instanceId: INSTANCE,
|
|
54
|
+
revision: revBase + i * 2,
|
|
55
|
+
eventType: 'task.status',
|
|
56
|
+
eventTime: new Date(started).toISOString(),
|
|
57
|
+
payload: { data: { taskId, status: 'in-progress', resourceRef: 'fk-1' } }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
domain: { id: domainId },
|
|
61
|
+
instanceId: INSTANCE,
|
|
62
|
+
revision: revBase + i * 2 + 1,
|
|
63
|
+
eventType: 'task.status',
|
|
64
|
+
eventTime: new Date(done).toISOString(),
|
|
65
|
+
/* 완료 어휘는 **커널이 소유한다**(`foldTaskRecords`) — 'done' 이 아니라 'completed' 다.
|
|
66
|
+
처음에 'done' 으로 심었더니 창은 잡히는데 건수가 0 이었다(이 DB 스펙이 그것을 잡았다). */
|
|
67
|
+
payload: { data: { taskId, status: 'completed', resourceRef: 'fk-1' } }
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return rows
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
before(async () => {
|
|
75
|
+
ds = new TypeOrmDataSource({
|
|
76
|
+
type: 'sqlite',
|
|
77
|
+
database: ':memory:',
|
|
78
|
+
synchronize: true,
|
|
79
|
+
dropSchema: true,
|
|
80
|
+
namingStrategy: new NamingStrategy(),
|
|
81
|
+
entities: [...shellEntities, ...authEntities, ...twinEntities],
|
|
82
|
+
logging: false
|
|
83
|
+
})
|
|
84
|
+
await ds.initialize()
|
|
85
|
+
addDataSource('default', ds)
|
|
86
|
+
const d = await ds.getRepository(Domain).save({ name: 'baseline-test', subdomain: 'baseline-test' } as any)
|
|
87
|
+
domainId = d.id
|
|
88
|
+
|
|
89
|
+
await ds.getRepository(TwinInstance).save({
|
|
90
|
+
domain: { id: domainId },
|
|
91
|
+
instanceId: INSTANCE,
|
|
92
|
+
kind: 'wms',
|
|
93
|
+
status: 'stopped',
|
|
94
|
+
board: { locations: [{ id: 'dock', type: 'dock', capacity: 10 }], equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock' }] }
|
|
95
|
+
} as any)
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* 지금 창 20건 · 어제 40건 · 그제 60건 · **3일 전은 기록 없음** · 4일 전 80건.
|
|
99
|
+
* 3일 전을 비워 두는 것이 이 스펙의 요점이다 — 그 날이 결측으로 남고 분포에서 빠지는지 본다
|
|
100
|
+
* (조용히 빼면 "지난 4일" 이 실제로는 3일이었다는 것을 아무도 모른다).
|
|
101
|
+
*/
|
|
102
|
+
const rows = [
|
|
103
|
+
...taskRows(0, 20, 30_000, 1000),
|
|
104
|
+
...taskRows(1, 40, 40_000, 3000),
|
|
105
|
+
...taskRows(2, 60, 50_000, 6000),
|
|
106
|
+
...taskRows(4, 80, 60_000, 9000)
|
|
107
|
+
]
|
|
108
|
+
await ds.getRepository(TwinEvent).save(rows)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
after(async () => {
|
|
112
|
+
await ds?.destroy()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const kpi = (periods: number, stride?: 'day' | 'week') =>
|
|
116
|
+
computeTwinKpi({
|
|
117
|
+
domainId,
|
|
118
|
+
instanceId: INSTANCE,
|
|
119
|
+
fromTime: new Date(FROM).toISOString(),
|
|
120
|
+
toTime: new Date(TO).toISOString(),
|
|
121
|
+
baseline: { periods, ...(stride ? { stride } : {}) }
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('과거 창을 하루씩 따로 읽어 분포를 만든다', async () => {
|
|
125
|
+
const out: any = await kpi(4)
|
|
126
|
+
const tasks = out.baseline.metrics.find((m: any) => m.metric === 'throughput.tasks')
|
|
127
|
+
|
|
128
|
+
assert.equal(out.baseline.stride, 'day')
|
|
129
|
+
assert.equal(out.baseline.periods, 4)
|
|
130
|
+
assert.equal(tasks.current, 20, '지금 창')
|
|
131
|
+
/* 어제 40 · 그제 60 · 3일 전 없음 · 4일 전 80. */
|
|
132
|
+
assert.deepEqual([tasks.min, tasks.median, tasks.max], [40, 60, 80])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('기록 없는 날을 **결측으로 남긴다** — 요청한 창 수와 측정된 수를 함께 답한다', async () => {
|
|
136
|
+
const out: any = await kpi(4)
|
|
137
|
+
const tasks = out.baseline.metrics.find((m: any) => m.metric === 'throughput.tasks')
|
|
138
|
+
|
|
139
|
+
assert.equal(tasks.requested, 4)
|
|
140
|
+
assert.equal(tasks.measuredCount, 3, '3일 전은 기록이 없다')
|
|
141
|
+
assert.equal(tasks.samples.length, 4, '창 자체는 네 개 모두 돌려준다')
|
|
142
|
+
assert.deepEqual(tasks.samples.map((s: any) => s.measured), [true, true, false, true])
|
|
143
|
+
assert.deepEqual(tasks.samples.map((s: any) => s.value), [40, 60, undefined, 80])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('처리량이 지난 며칠 중 가장 낮으면 worst — 이 축이 답하려던 질문', async () => {
|
|
147
|
+
/* 예전에는 "어제보다 나아졌나" 까지만 답할 수 있었다. 지금 창(20)은 어제(40)보다도 낮고
|
|
148
|
+
최근 어느 날보다도 낮다 — 그것을 한 번에 말할 수 있어야 한다. */
|
|
149
|
+
const out: any = await kpi(4)
|
|
150
|
+
const tasks = out.baseline.metrics.find((m: any) => m.metric === 'throughput.tasks')
|
|
151
|
+
|
|
152
|
+
assert.equal(tasks.standing, 'worst')
|
|
153
|
+
assert.equal(tasks.rank, 1, '오름차순 첫째')
|
|
154
|
+
assert.equal(tasks.rankOf, 4, '표본 3 + 현재 1')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('방향은 지표가 안다 — 작업시간이 가장 짧으면 best', async () => {
|
|
158
|
+
const out: any = await kpi(4)
|
|
159
|
+
const work = out.baseline.metrics.find((m: any) => m.metric === 'workTime.p50Ms')
|
|
160
|
+
|
|
161
|
+
assert.equal(work.current, 30_000)
|
|
162
|
+
assert.deepEqual([work.min, work.max], [40_000, 60_000])
|
|
163
|
+
assert.equal(work.standing, 'best', '짧을수록 좋다')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('구간 수를 줄이면 그만큼만 본다 — 창을 지어내지 않는다', async () => {
|
|
167
|
+
const out: any = await kpi(1)
|
|
168
|
+
const tasks = out.baseline.metrics.find((m: any) => m.metric === 'throughput.tasks')
|
|
169
|
+
|
|
170
|
+
assert.equal(tasks.requested, 1)
|
|
171
|
+
assert.equal(tasks.measuredCount, 1)
|
|
172
|
+
/* 표본 하나는 분포가 아니다. */
|
|
173
|
+
assert.equal(tasks.noPosition, 'too-few-samples')
|
|
174
|
+
assert.equal(tasks.standing, undefined)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('주 단위는 같은 요일을 본다 — 하루 전 기록에 걸리지 않는다', async () => {
|
|
178
|
+
const out: any = await kpi(2, 'week')
|
|
179
|
+
const tasks = out.baseline.metrics.find((m: any) => m.metric === 'throughput.tasks')
|
|
180
|
+
|
|
181
|
+
assert.equal(out.baseline.stride, 'week')
|
|
182
|
+
/* 심어 둔 기록은 최근 나흘뿐이라 지난주 같은 요일에는 아무것도 없다 — 지어내지 않는다. */
|
|
183
|
+
assert.equal(tasks.measuredCount, 0)
|
|
184
|
+
assert.equal(tasks.noPosition, 'too-few-samples')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('요청하지 않으면 분포를 만들지 않는다 — 조회가 공짜가 아니다', async () => {
|
|
188
|
+
const out: any = await computeTwinKpi({
|
|
189
|
+
domainId,
|
|
190
|
+
instanceId: INSTANCE,
|
|
191
|
+
fromTime: new Date(FROM).toISOString(),
|
|
192
|
+
toTime: new Date(TO).toISOString()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
assert.equal(out.baseline, undefined)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('지금 창에 기록이 없어도 분포는 낸다 — 그때가 가장 알고 싶은 순간이다', async () => {
|
|
199
|
+
/* 심어 둔 기록이 없는 시각을 창으로 잡는다(3일 전). 평소엔 일이 있었는데 지금 없다 = 멈춘 신호. */
|
|
200
|
+
const quietTo = TO - 3 * MS_DAY
|
|
201
|
+
const out: any = await computeTwinKpi({
|
|
202
|
+
domainId,
|
|
203
|
+
instanceId: INSTANCE,
|
|
204
|
+
fromTime: new Date(quietTo - 60 * 60_000).toISOString(),
|
|
205
|
+
toTime: new Date(quietTo).toISOString(),
|
|
206
|
+
baseline: { periods: 2 }
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
assert.equal(out.measured, false, '지금 창에는 측정할 기록이 없다')
|
|
210
|
+
assert.ok(out.baseline, '그래도 분포는 낸다')
|
|
211
|
+
const tasks = out.baseline.metrics.find((m: any) => m.metric === 'throughput.tasks')
|
|
212
|
+
assert.equal(tasks.measuredCount, 1, '4일 전 하루만 걸린다(5일 전은 기록 없음)')
|
|
213
|
+
assert.equal(tasks.samples.find((s: any) => s.measured).value, 80)
|
|
214
|
+
})
|