@operato/scene-storage 10.0.0-beta.28 → 10.0.0-beta.31

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 (58) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/asrs-crane-3d.d.ts +10 -0
  3. package/dist/asrs-crane-3d.js +17 -0
  4. package/dist/asrs-crane-3d.js.map +1 -1
  5. package/dist/asrs-crane.d.ts +58 -13
  6. package/dist/asrs-crane.js +120 -16
  7. package/dist/asrs-crane.js.map +1 -1
  8. package/dist/asrs-rack.d.ts +58 -19
  9. package/dist/asrs-rack.js +107 -20
  10. package/dist/asrs-rack.js.map +1 -1
  11. package/dist/box.d.ts +10 -3
  12. package/dist/box.js +1 -2
  13. package/dist/box.js.map +1 -1
  14. package/dist/generic-container-3d.js.map +1 -1
  15. package/dist/generic-container.d.ts +12 -2
  16. package/dist/generic-container.js +1 -2
  17. package/dist/generic-container.js.map +1 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/pallet.d.ts +9 -2
  22. package/dist/pallet.js +1 -2
  23. package/dist/pallet.js.map +1 -1
  24. package/dist/parcel.d.ts +10 -3
  25. package/dist/parcel.js +1 -2
  26. package/dist/parcel.js.map +1 -1
  27. package/dist/rack-cell-3d.d.ts +25 -0
  28. package/dist/rack-cell-3d.js +88 -0
  29. package/dist/rack-cell-3d.js.map +1 -0
  30. package/dist/rack-cell.d.ts +64 -0
  31. package/dist/rack-cell.js +197 -0
  32. package/dist/rack-cell.js.map +1 -0
  33. package/dist/spot-3d.js.map +1 -1
  34. package/dist/spot.d.ts +12 -11
  35. package/dist/spot.js +2 -3
  36. package/dist/spot.js.map +1 -1
  37. package/dist/templates/index.d.ts +42 -0
  38. package/dist/templates/index.js +43 -1
  39. package/dist/templates/index.js.map +1 -1
  40. package/package.json +9 -4
  41. package/src/asrs-crane-3d.ts +20 -0
  42. package/src/asrs-crane.ts +153 -17
  43. package/src/asrs-rack.ts +137 -22
  44. package/src/box.ts +15 -5
  45. package/src/generic-container-3d.ts +1 -1
  46. package/src/generic-container.ts +22 -7
  47. package/src/index.ts +3 -0
  48. package/src/pallet.ts +16 -6
  49. package/src/parcel.ts +15 -5
  50. package/src/rack-cell-3d.ts +101 -0
  51. package/src/rack-cell.ts +241 -0
  52. package/src/spot-3d.ts +1 -1
  53. package/src/spot.ts +17 -7
  54. package/src/templates/index.ts +43 -1
  55. package/test/setup.js +279 -0
  56. package/test/test-asrs-crane.ts +319 -0
  57. package/tsconfig.json +2 -1
  58. package/tsconfig.tsbuildinfo +1 -1
package/test/setup.js ADDED
@@ -0,0 +1,279 @@
1
+ /*
2
+ * Browser polyfills for Node.js test environment.
3
+ * Required when importing @hatiolab/things-scene bundle which uses browser globals at module scope.
4
+ */
5
+
6
+ var noop = function () {}
7
+
8
+ // Image polyfill (used at module scope in move-handle.ts)
9
+ if (typeof global.Image === 'undefined') {
10
+ global.Image = class Image {
11
+ constructor() {
12
+ this.onload = null
13
+ this.onerror = null
14
+ this.src = ''
15
+ this.width = 0
16
+ this.height = 0
17
+ }
18
+ set src(v) { this._src = v }
19
+ get src() { return this._src || '' }
20
+ addEventListener(evt, fn) {}
21
+ removeEventListener() {}
22
+ }
23
+ }
24
+
25
+ // HTMLCanvasElement polyfill
26
+ if (typeof global.HTMLCanvasElement === 'undefined') {
27
+ global.HTMLCanvasElement = class HTMLCanvasElement {
28
+ constructor() { this.width = 0; this.height = 0 }
29
+ getContext() { return null }
30
+ toDataURL() { return '' }
31
+ addEventListener() {}
32
+ removeEventListener() {}
33
+ }
34
+ }
35
+
36
+ // HTMLElement / HTMLVideoElement stubs
37
+ if (typeof global.HTMLElement === 'undefined') {
38
+ global.HTMLElement = class HTMLElement {}
39
+ }
40
+ if (typeof global.HTMLVideoElement === 'undefined') {
41
+ global.HTMLVideoElement = class HTMLVideoElement {}
42
+ }
43
+
44
+ // MutationObserver polyfill
45
+ if (typeof global.MutationObserver === 'undefined') {
46
+ global.MutationObserver = class MutationObserver {
47
+ constructor(callback) {}
48
+ observe() {}
49
+ disconnect() {}
50
+ takeRecords() { return [] }
51
+ }
52
+ }
53
+
54
+ // ResizeObserver polyfill
55
+ if (typeof global.ResizeObserver === 'undefined') {
56
+ global.ResizeObserver = class ResizeObserver {
57
+ constructor(callback) {}
58
+ observe() {}
59
+ unobserve() {}
60
+ disconnect() {}
61
+ }
62
+ }
63
+
64
+ // Mini-DOM Node implementation
65
+ function createNode(nodeType, tagName) {
66
+ var node = {
67
+ nodeType: nodeType,
68
+ tagName: tagName || '',
69
+ parentNode: null,
70
+ parentElement: null,
71
+ childNodes: [],
72
+ firstChild: null,
73
+ lastChild: null,
74
+ nextSibling: null,
75
+ previousSibling: null,
76
+ style: nodeType === 1 ? new Proxy({}, {
77
+ set: function(t,p,v){ t[p]=v; return true },
78
+ get: function(t,p){ if(p==='setProperty') return function(k,v){ t[k]=v }; return t[p] || '' }
79
+ }) : {},
80
+ textContent: '',
81
+ data: '',
82
+ setAttribute: noop,
83
+ getAttribute: function () { return null },
84
+ removeAttribute: noop,
85
+ addEventListener: noop,
86
+ removeEventListener: noop,
87
+ getContext: function() { return null },
88
+ contains: function () { return false },
89
+ querySelector: function() { return null },
90
+ querySelectorAll: function() { return [] },
91
+
92
+ appendChild: function (child) {
93
+ if (child.nodeType === 11) {
94
+ while (child.childNodes.length > 0) node.appendChild(child.childNodes[0])
95
+ return child
96
+ }
97
+ if (child.parentNode) child.parentNode.removeChild(child)
98
+ node.childNodes.push(child)
99
+ child.parentNode = node
100
+ child.parentElement = nodeType === 1 ? node : null
101
+ if (node.childNodes.length > 1) {
102
+ var prev = node.childNodes[node.childNodes.length - 2]
103
+ prev.nextSibling = child
104
+ child.previousSibling = prev
105
+ }
106
+ child.nextSibling = null
107
+ node.firstChild = node.childNodes[0]
108
+ node.lastChild = child
109
+ return child
110
+ },
111
+
112
+ removeChild: function (child) {
113
+ var idx = node.childNodes.indexOf(child)
114
+ if (idx === -1) return child
115
+ node.childNodes.splice(idx, 1)
116
+ if (child.previousSibling) child.previousSibling.nextSibling = child.nextSibling
117
+ if (child.nextSibling) child.nextSibling.previousSibling = child.previousSibling
118
+ child.parentNode = null; child.parentElement = null
119
+ child.previousSibling = null; child.nextSibling = null
120
+ node.firstChild = node.childNodes[0] || null
121
+ node.lastChild = node.childNodes[node.childNodes.length - 1] || null
122
+ return child
123
+ },
124
+
125
+ remove: function () { if (node.parentNode) node.parentNode.removeChild(node) },
126
+
127
+ insertBefore: function (newChild, refChild) {
128
+ if (!refChild) return node.appendChild(newChild)
129
+ if (newChild.nodeType === 11) {
130
+ var children = newChild.childNodes.slice()
131
+ for (var i = 0; i < children.length; i++) node.insertBefore(children[i], refChild)
132
+ return newChild
133
+ }
134
+ if (newChild.parentNode) newChild.parentNode.removeChild(newChild)
135
+ var idx = node.childNodes.indexOf(refChild)
136
+ if (idx === -1) return node.appendChild(newChild)
137
+ node.childNodes.splice(idx, 0, newChild)
138
+ newChild.parentNode = node
139
+ newChild.parentElement = nodeType === 1 ? node : null
140
+ newChild.nextSibling = refChild
141
+ newChild.previousSibling = refChild.previousSibling
142
+ if (refChild.previousSibling) refChild.previousSibling.nextSibling = newChild
143
+ refChild.previousSibling = newChild
144
+ node.firstChild = node.childNodes[0]
145
+ node.lastChild = node.childNodes[node.childNodes.length - 1]
146
+ return newChild
147
+ },
148
+
149
+ cloneNode: function (deep) {
150
+ var clone = createNode(nodeType, tagName)
151
+ clone.data = node.data; clone.textContent = node.textContent
152
+ if (deep && node.childNodes.length) {
153
+ for (var i = 0; i < node.childNodes.length; i++) clone.appendChild(node.childNodes[i].cloneNode(true))
154
+ }
155
+ return clone
156
+ },
157
+
158
+ replaceChild: function (newChild, oldChild) {
159
+ node.insertBefore(newChild, oldChild); node.removeChild(oldChild); return oldChild
160
+ }
161
+ }
162
+
163
+ if (nodeType === 1) {
164
+ node.children = []
165
+ if (tagName === 'template') {
166
+ node.content = createNode(11, '')
167
+ Object.defineProperty(node, 'innerHTML', {
168
+ get: function () { return '' },
169
+ set: function (html) { node.content = createNode(11, '') }
170
+ })
171
+ } else {
172
+ node.innerHTML = ''
173
+ }
174
+ }
175
+ return node
176
+ }
177
+
178
+ // document polyfill
179
+ if (typeof global.document === 'undefined') {
180
+ global.document = {
181
+ documentElement: {},
182
+ fonts: { ready: Promise.resolve() },
183
+ createElement: function (tagName) { return createNode(1, tagName) },
184
+ createComment: function (data) { var n = createNode(8, ''); n.data = data || ''; return n },
185
+ createTextNode: function (data) { var n = createNode(3, ''); n.data = data || ''; return n },
186
+ createDocumentFragment: function () { return createNode(11, '') },
187
+ createTreeWalker: function (root, whatToShow) {
188
+ var walker = {
189
+ currentNode: root,
190
+ nextNode: function () {
191
+ var cur = walker.currentNode
192
+ if (!cur) return null
193
+ if (cur.childNodes && cur.childNodes.length > 0) { walker.currentNode = cur.childNodes[0]; return walker.currentNode }
194
+ var node = cur
195
+ while (node && node !== root) {
196
+ if (node.nextSibling) { walker.currentNode = node.nextSibling; return walker.currentNode }
197
+ node = node.parentNode
198
+ }
199
+ return null
200
+ }
201
+ }
202
+ return walker
203
+ },
204
+ importNode: function (node, deep) { return node && node.cloneNode ? node.cloneNode(deep) : node },
205
+ adoptNode: function (node) { return node },
206
+ querySelector: function() { return null },
207
+ querySelectorAll: function() { return [] },
208
+ body: createNode(1, 'body'),
209
+ head: createNode(1, 'head')
210
+ }
211
+ }
212
+
213
+ // getComputedStyle polyfill
214
+ if (typeof global.getComputedStyle !== 'function') {
215
+ global.getComputedStyle = function () {
216
+ return { getPropertyValue: function () { return '' }, fontFamily: 'sans-serif' }
217
+ }
218
+ }
219
+
220
+ // window polyfill
221
+ if (typeof global.window === 'undefined') {
222
+ global.window = global
223
+ }
224
+
225
+ global.addEventListener = global.addEventListener || noop
226
+ global.removeEventListener = global.removeEventListener || noop
227
+
228
+ if (!global.window.location) {
229
+ global.window.location = { hostname: 'localhost', href: 'http://localhost/' }
230
+ }
231
+
232
+ // screen polyfill
233
+ if (typeof global.screen === 'undefined') {
234
+ global.screen = { width: 1280, height: 800 }
235
+ }
236
+
237
+ // requestAnimationFrame polyfill
238
+ if (typeof global.requestAnimationFrame === 'undefined') {
239
+ var lastTime = 0
240
+ global.requestAnimationFrame = function (callback) {
241
+ var currTime = Date.now()
242
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime))
243
+ var id = setTimeout(function () { callback(currTime + timeToCall) }, timeToCall)
244
+ lastTime = currTime + timeToCall
245
+ return id
246
+ }
247
+ global.cancelAnimationFrame = function (id) { clearTimeout(id) }
248
+ }
249
+
250
+ // performance polyfill
251
+ if (typeof global.performance === 'undefined') {
252
+ global.performance = {}
253
+ }
254
+ if (!global.performance.now) {
255
+ var nowOffset = Date.now()
256
+ global.performance.now = function () { return Date.now() - nowOffset }
257
+ }
258
+
259
+ // WebGL stub (Three.js renderer check)
260
+ if (typeof global.WebGLRenderingContext === 'undefined') {
261
+ global.WebGLRenderingContext = class WebGLRenderingContext {}
262
+ }
263
+
264
+ // CSS polyfill
265
+ if (typeof global.CSS === 'undefined') {
266
+ global.CSS = { supports: function() { return false } }
267
+ }
268
+
269
+ // CustomEvent polyfill
270
+ if (typeof global.CustomEvent === 'undefined') {
271
+ global.CustomEvent = class CustomEvent {
272
+ constructor(type, init) { this.type = type; this.detail = (init || {}).detail }
273
+ }
274
+ }
275
+
276
+ // URL polyfill (Node.js has this but in some versions it may be missing from global)
277
+ if (typeof global.URL === 'undefined') {
278
+ global.URL = require('url').URL
279
+ }
@@ -0,0 +1,319 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * AsrsCrane integration tests — pick/place data-flow for the ASRS crane.
5
+ *
6
+ * Uses fake/minimal containers (no DOM, no Three.js, no RAF) so they run in
7
+ * Node with tsx.
8
+ *
9
+ * Test infrastructure mirrors test-transfer-scenarios.ts in scene-base:
10
+ * - FakeBase: minimal child-tracking container with setState support
11
+ * - FakeCrane: Mover(ContainerCapacity(FakeBase)) — instant moveTo, AsrsCrane
12
+ * engage() semantics (status + carriageHeight), slots = [{id:'forks', maxCount:1}]
13
+ * - FakeRackCell: mimics RackCell.receive / dispatch / canReceive protocol
14
+ */
15
+
16
+ import 'should'
17
+ import { ContainerCapacity, TRANSFER_SLOT_KEY } from '@hatiolab/things-scene'
18
+ import Mover from '../../scene-base/src/mover.js'
19
+
20
+ // ── Shared fake infrastructure ─────────────────────────────────────────────────
21
+
22
+ class FakeBase {
23
+ _components: any[] = []
24
+ state: Record<string, any>
25
+ parent: any = null
26
+ root: any = null
27
+
28
+ constructor(state: Record<string, any> = {}) {
29
+ this.state = state
30
+ }
31
+
32
+ get components() {
33
+ return this._components
34
+ }
35
+
36
+ setState(keyOrObj: string | Record<string, any>, value?: any) {
37
+ if (typeof keyOrObj === 'object') {
38
+ Object.assign(this.state, keyOrObj)
39
+ } else {
40
+ this.state[keyOrObj as string] = value
41
+ }
42
+ }
43
+
44
+ getState(key: string) {
45
+ return this.state[key]
46
+ }
47
+
48
+ addComponent(child: any) {
49
+ if (child.parent && child.parent !== this) {
50
+ const idx = child.parent._components?.indexOf(child) ?? -1
51
+ if (idx >= 0) child.parent._components.splice(idx, 1)
52
+ child.parent = null
53
+ }
54
+ if (!this._components.includes(child)) {
55
+ this._components.push(child)
56
+ }
57
+ child.parent = this
58
+ }
59
+
60
+ removeComponent(child: any) {
61
+ const idx = this._components.indexOf(child)
62
+ if (idx >= 0) this._components.splice(idx, 1)
63
+ if (child.parent === this) child.parent = null
64
+ }
65
+
66
+ reparent(child: any, _options?: any) {
67
+ this.addComponent(child)
68
+ }
69
+
70
+ trigger(_name: string, ..._args: any[]) {}
71
+ }
72
+
73
+ function makeCarrier(zPos?: number) {
74
+ return {
75
+ parent: null as any,
76
+ state: { type: 'pallet', ...(zPos !== undefined ? { zPos } : {}) },
77
+ type: 'pallet',
78
+ [TRANSFER_SLOT_KEY]: undefined as any,
79
+ setState(_s: any) {}
80
+ }
81
+ }
82
+
83
+ // FakeCrane: Mover(ContainerCapacity(FakeBase)) with instant moveTo +
84
+ // AsrsCrane engage() semantics (status + carriageHeight snap from carrier.state.zPos).
85
+ const FakeCraneBase = Mover(ContainerCapacity(FakeBase as any))
86
+
87
+ class FakeCrane extends (FakeCraneBase as any) {
88
+ constructor(state: Record<string, any> = {}) {
89
+ super({ status: 'idle', carriageHeight: 0, ...state })
90
+ }
91
+
92
+ get slots() {
93
+ return [{ id: 'forks', maxCount: 1 }]
94
+ }
95
+
96
+ moveTo(_target: any, _options: any = {}): Promise<void> {
97
+ return Promise.resolve()
98
+ }
99
+
100
+ async engage(target: any, kind: 'pick' | 'place'): Promise<void> {
101
+ if (kind === 'pick') {
102
+ this.state.status = 'loading'
103
+ const carrierY = target?.state?.zPos ?? null
104
+ if (carrierY !== null) this.state.carriageHeight = carrierY
105
+ } else {
106
+ this.state.status = 'unloading'
107
+ }
108
+ }
109
+
110
+ /** Semantic alias for pick (matches AsrsCrane.fetch). */
111
+ fetch(carrier: any, options?: any): Promise<void> {
112
+ return (this as any).pick(carrier, options)
113
+ }
114
+
115
+ /** Semantic alias for place (matches AsrsCrane.deposit). */
116
+ deposit(carrier: any, cell: any, options?: any): Promise<void> {
117
+ return (this as any).place(carrier, cell, options)
118
+ }
119
+ }
120
+
121
+ // FakeRackCell: mimics RackCell.receive / dispatch / canReceive protocol.
122
+ // capacity=1 (single-slot cell).
123
+ class FakeRackCell extends FakeBase {
124
+ cellId: string
125
+
126
+ constructor(state: Record<string, any> = {}) {
127
+ super(state)
128
+ this.cellId = (state.cellId as string) || 'cell-0-0-0'
129
+ }
130
+
131
+ canReceive(_component?: any): boolean {
132
+ return this._components.length < 1
133
+ }
134
+
135
+ async receive(carrier: any, options: any = {}): Promise<void> {
136
+ if (!this.canReceive(carrier)) return
137
+ carrier[TRANSFER_SLOT_KEY] = this.cellId
138
+ this.reparent(carrier, options)
139
+ }
140
+
141
+ async dispatch(carrier: any, target: any, options: any = {}): Promise<void> {
142
+ this.removeComponent(carrier)
143
+ if (typeof target?.receive === 'function') {
144
+ await target.receive(carrier, options)
145
+ } else {
146
+ target?.reparent?.(carrier, options)
147
+ }
148
+ }
149
+ }
150
+
151
+ // ── Scenario 1: fetch (pick) ──────────────────────────────────────────────────
152
+
153
+ describe('AsrsCrane: fetch (pick)', () => {
154
+ it('fetch 후 carrier가 crane의 child', async () => {
155
+ const crane = new FakeCrane()
156
+ const carrier = makeCarrier()
157
+
158
+ await crane.fetch(carrier)
159
+
160
+ crane._components.should.containEql(carrier)
161
+ })
162
+
163
+ it('fetch 후 TRANSFER_SLOT_KEY = forks', async () => {
164
+ const crane = new FakeCrane()
165
+ const carrier = makeCarrier()
166
+
167
+ await crane.fetch(carrier)
168
+
169
+ carrier[TRANSFER_SLOT_KEY].should.equal('forks')
170
+ })
171
+
172
+ it('fetch engage → status loading', async () => {
173
+ const crane = new FakeCrane()
174
+ const carrier = makeCarrier()
175
+
176
+ await crane.fetch(carrier)
177
+
178
+ crane.state.status.should.equal('loading')
179
+ })
180
+
181
+ it('carrier에 zPos 있으면 carriageHeight 스냅', async () => {
182
+ const crane = new FakeCrane()
183
+ const carrier = makeCarrier(1500)
184
+
185
+ await crane.fetch(carrier)
186
+
187
+ crane.state.carriageHeight.should.equal(1500)
188
+ })
189
+
190
+ it('zPos 없으면 carriageHeight 변경 없음', async () => {
191
+ const crane = new FakeCrane({ carriageHeight: 500 })
192
+ const carrier = makeCarrier() // no zPos
193
+
194
+ await crane.fetch(carrier)
195
+
196
+ crane.state.carriageHeight.should.equal(500)
197
+ })
198
+
199
+ it('fetch 전에 carrier가 rack cell에 있었으면 거기서 제거', async () => {
200
+ const crane = new FakeCrane()
201
+ const cell = new FakeRackCell({ cellId: 'cell-0-0-0' })
202
+ const carrier = makeCarrier()
203
+ await cell.receive(carrier)
204
+
205
+ await crane.fetch(carrier)
206
+
207
+ crane._components.should.containEql(carrier)
208
+ cell._components.should.not.containEql(carrier)
209
+ })
210
+
211
+ it('maxCount(1) 가득 차면 canReceive false', async () => {
212
+ const crane = new FakeCrane()
213
+ const c1 = makeCarrier()
214
+ const c2 = makeCarrier()
215
+
216
+ await crane.fetch(c1)
217
+ ;(crane as any).canReceive(c2).should.be.false()
218
+ })
219
+ })
220
+
221
+ // ── Scenario 2: deposit (place) ───────────────────────────────────────────────
222
+
223
+ describe('AsrsCrane: deposit (place)', () => {
224
+ it('deposit 후 carrier가 rackCell의 child', async () => {
225
+ const crane = new FakeCrane()
226
+ const cell = new FakeRackCell({ cellId: 'cell-1-0-2' })
227
+ const carrier = makeCarrier()
228
+
229
+ crane.addComponent(carrier)
230
+ carrier[TRANSFER_SLOT_KEY] = 'forks'
231
+
232
+ await crane.deposit(carrier, cell)
233
+
234
+ cell._components.should.containEql(carrier)
235
+ crane._components.should.not.containEql(carrier)
236
+ })
237
+
238
+ it('deposit engage → status unloading', async () => {
239
+ const crane = new FakeCrane()
240
+ const cell = new FakeRackCell()
241
+ const carrier = makeCarrier()
242
+
243
+ crane.addComponent(carrier)
244
+ carrier[TRANSFER_SLOT_KEY] = 'forks'
245
+
246
+ await crane.deposit(carrier, cell)
247
+
248
+ crane.state.status.should.equal('unloading')
249
+ })
250
+
251
+ it('deposit 후 TRANSFER_SLOT_KEY = cellId', async () => {
252
+ const crane = new FakeCrane()
253
+ const cell = new FakeRackCell({ cellId: 'cell-3-0-1' })
254
+ const carrier = makeCarrier()
255
+
256
+ crane.addComponent(carrier)
257
+ carrier[TRANSFER_SLOT_KEY] = 'forks'
258
+
259
+ await crane.deposit(carrier, cell)
260
+
261
+ carrier[TRANSFER_SLOT_KEY].should.equal('cell-3-0-1')
262
+ })
263
+ })
264
+
265
+ // ── Scenario 3: fetch → deposit 전체 사이클 ──────────────────────────────────
266
+
267
+ describe('AsrsCrane: fetch → deposit 전체 사이클', () => {
268
+ it('cell-A → crane → cell-B', async () => {
269
+ const crane = new FakeCrane()
270
+ const cellA = new FakeRackCell({ cellId: 'cell-0-0-0' })
271
+ const cellB = new FakeRackCell({ cellId: 'cell-5-0-3' })
272
+ const carrier = makeCarrier()
273
+
274
+ // Pre-place carrier in cellA
275
+ await cellA.receive(carrier)
276
+ cellA._components.should.containEql(carrier)
277
+ carrier[TRANSFER_SLOT_KEY].should.equal('cell-0-0-0')
278
+
279
+ // Fetch from cellA
280
+ await crane.fetch(carrier)
281
+ crane._components.should.containEql(carrier)
282
+ cellA._components.should.not.containEql(carrier)
283
+ carrier[TRANSFER_SLOT_KEY].should.equal('forks')
284
+
285
+ // Deposit into cellB
286
+ await crane.deposit(carrier, cellB)
287
+ cellB._components.should.containEql(carrier)
288
+ crane._components.should.not.containEql(carrier)
289
+ carrier[TRANSFER_SLOT_KEY].should.equal('cell-5-0-3')
290
+ })
291
+
292
+ it('fetch → deposit 후 status 순서: loading → unloading', async () => {
293
+ const crane = new FakeCrane()
294
+ const cell = new FakeRackCell({ cellId: 'cell-0-0-0' })
295
+ const carrier = makeCarrier()
296
+
297
+ await crane.fetch(carrier)
298
+ crane.state.status.should.equal('loading')
299
+
300
+ await crane.deposit(carrier, cell)
301
+ crane.state.status.should.equal('unloading')
302
+ })
303
+
304
+ it('pickAndPlace: carrier를 cellA에서 cellB로 한 번에', async () => {
305
+ const crane = new FakeCrane()
306
+ const cellA = new FakeRackCell({ cellId: 'cell-2-0-1' })
307
+ const cellB = new FakeRackCell({ cellId: 'cell-7-0-3' })
308
+ const carrier = makeCarrier()
309
+
310
+ await cellA.receive(carrier)
311
+
312
+ await (crane as any).pickAndPlace(carrier, cellB)
313
+
314
+ cellB._components.should.containEql(carrier)
315
+ cellA._components.should.not.containEql(carrier)
316
+ crane._components.should.not.containEql(carrier)
317
+ carrier[TRANSFER_SLOT_KEY].should.equal('cell-7-0-3')
318
+ })
319
+ })
package/tsconfig.json CHANGED
@@ -19,5 +19,6 @@
19
19
  "incremental": true,
20
20
  "skipLibCheck": true
21
21
  },
22
- "include": ["**/*.ts", "*.d.ts"]
22
+ "include": ["src/**/*.ts"],
23
+ "exclude": ["dist/**", "test/**", "node_modules"]
23
24
  }