@shipload/sdk 1.0.0-next.7 → 1.0.0-next.71
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/lib/scan.d.ts +52 -0
- package/lib/scan.js +162 -0
- package/lib/scan.js.map +1 -0
- package/lib/scan.m.js +152 -0
- package/lib/scan.m.js.map +1 -0
- package/lib/shipload.d.ts +5822 -1484
- package/lib/shipload.js +20838 -5419
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +20674 -5669
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +1691 -0
- package/lib/testing.js +7109 -0
- package/lib/testing.js.map +1 -0
- package/lib/testing.m.js +7103 -0
- package/lib/testing.m.js.map +1 -0
- package/package.json +20 -2
- package/src/capabilities/craftable.test.ts +82 -0
- package/src/capabilities/craftable.ts +82 -0
- package/src/capabilities/crafting.test.ts +80 -0
- package/src/capabilities/crafting.ts +50 -8
- package/src/capabilities/gathering.test.ts +29 -0
- package/src/capabilities/gathering.ts +38 -20
- package/src/capabilities/hauling.ts +0 -5
- package/src/capabilities/index.ts +0 -1
- package/src/capabilities/modules.ts +27 -30
- package/src/capabilities/movement.ts +1 -1
- package/src/capabilities/storage.test.ts +26 -0
- package/src/capabilities/storage.ts +22 -7
- package/src/contracts/fund.ts +306 -0
- package/src/contracts/platform.ts +312 -3
- package/src/contracts/server.ts +2793 -478
- package/src/contracts/token.ts +170 -0
- package/src/contracts.ts +2 -0
- package/src/coordinates/address.ts +88 -0
- package/src/coordinates/constants.test.ts +15 -0
- package/src/coordinates/constants.ts +23 -0
- package/src/coordinates/index.ts +15 -0
- package/src/coordinates/memo.test.ts +47 -0
- package/src/coordinates/memo.ts +20 -0
- package/src/coordinates/permutation.ts +77 -0
- package/src/coordinates/regions.ts +48 -0
- package/src/coordinates/sectors.ts +115 -0
- package/src/data/capabilities.ts +49 -18
- package/src/data/capability-formulas.ts +26 -14
- package/src/data/catalog.test.ts +19 -0
- package/src/data/catalog.ts +13 -3
- package/src/data/charters.json +396 -0
- package/src/data/citizenry-adjectives.json +42 -0
- package/src/data/citizenry-nouns.json +50 -0
- package/src/data/colors.ts +14 -32
- package/src/data/entities.json +421 -15
- package/src/data/item-ids.ts +53 -13
- package/src/data/items.json +330 -77
- package/src/data/kind-registry.json +212 -0
- package/src/data/kind-registry.ts +192 -0
- package/src/data/metadata.ts +312 -51
- package/src/data/recipes-runtime.ts +6 -23
- package/src/data/recipes.json +2173 -173
- package/src/data/tiers.ts +9 -6
- package/src/derivation/build-methods.test.ts +71 -0
- package/src/derivation/build-methods.ts +50 -0
- package/src/derivation/capabilities.test.ts +348 -0
- package/src/derivation/capabilities.ts +707 -0
- package/src/derivation/capability-mappings.ts +60 -16
- package/src/derivation/crafting.test.ts +17 -0
- package/src/derivation/crafting.ts +57 -34
- package/src/derivation/index.ts +27 -2
- package/src/derivation/recipe-usage.test.ts +88 -0
- package/src/derivation/recipe-usage.ts +141 -0
- package/src/derivation/reserve-regen.ts +34 -0
- package/src/derivation/resources.ts +9 -1
- package/src/derivation/rollups.test.ts +55 -0
- package/src/derivation/rollups.ts +72 -0
- package/src/derivation/stars.test.ts +67 -0
- package/src/derivation/stars.ts +25 -0
- package/src/derivation/stat-scaling.ts +12 -0
- package/src/derivation/stats.ts +6 -6
- package/src/derivation/strata.ts +5 -3
- package/src/derivation/stratum.ts +26 -22
- package/src/derivation/tiers.ts +40 -7
- package/src/derivation/upgrades.ts +14 -0
- package/src/derivation/wormhole.ts +154 -0
- package/src/entities/entity.ts +102 -0
- package/src/entities/gamestate.ts +3 -28
- package/src/entities/makers.ts +144 -136
- package/src/entities/slot-multiplier.ts +43 -0
- package/src/entity/hydrate.test.ts +156 -0
- package/src/entity/hydrate.ts +86 -0
- package/src/errors.ts +21 -16
- package/src/format.ts +42 -9
- package/src/index-module.ts +425 -32
- package/src/influence/ballot.test.ts +103 -0
- package/src/influence/ballot.ts +98 -0
- package/src/influence/categories.ts +32 -0
- package/src/influence/charter-metadata.test.ts +136 -0
- package/src/influence/charter-metadata.ts +236 -0
- package/src/influence/charters.test.ts +112 -0
- package/src/influence/charters.ts +150 -0
- package/src/influence/citizenry.ts +38 -0
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +287 -0
- package/src/influence/civic.ts +269 -0
- package/src/influence/constants.ts +84 -0
- package/src/influence/decay.ts +20 -0
- package/src/influence/decomp.ts +95 -0
- package/src/influence/demand.ts +106 -0
- package/src/influence/duration.ts +101 -0
- package/src/influence/index.ts +118 -0
- package/src/influence/influence.test.ts +450 -0
- package/src/influence/quality.ts +25 -0
- package/src/influence/valuation.ts +154 -0
- package/src/managers/actions.ts +950 -93
- package/src/managers/balances.test.ts +161 -0
- package/src/managers/balances.ts +93 -0
- package/src/managers/base.ts +10 -2
- package/src/managers/cluster.test.ts +39 -0
- package/src/managers/cluster.ts +53 -0
- package/src/managers/construction-types.ts +80 -0
- package/src/managers/construction.ts +453 -0
- package/src/managers/context.ts +58 -1
- package/src/managers/coordinates.ts +14 -0
- package/src/managers/entities.ts +35 -63
- package/src/managers/epochs.ts +72 -1
- package/src/managers/index.ts +38 -1
- package/src/managers/influence.test.ts +56 -0
- package/src/managers/influence.ts +425 -0
- package/src/managers/jobs.test.ts +96 -0
- package/src/managers/jobs.ts +58 -0
- package/src/managers/locations.ts +25 -29
- package/src/managers/nft.test.ts +21 -0
- package/src/managers/nft.ts +110 -0
- package/src/managers/players.ts +25 -0
- package/src/managers/plot.ts +122 -0
- package/src/nft/atomicassets.abi.json +1342 -0
- package/src/nft/atomicassets.ts +237 -0
- package/src/nft/atomicdata.ts +130 -0
- package/src/nft/buildImmutableData.ts +388 -0
- package/src/nft/description.ts +222 -100
- package/src/nft/index.ts +3 -0
- package/src/planner/index.ts +296 -0
- package/src/planner/planner.test.ts +459 -0
- package/src/receipts.ts +22 -0
- package/src/resolution/describe-module.ts +43 -25
- package/src/resolution/display-name.ts +39 -11
- package/src/resolution/resolve-item.test.ts +48 -0
- package/src/resolution/resolve-item.ts +112 -58
- package/src/scan/index.ts +244 -0
- package/src/scan/scan-wasm.base64.ts +2 -0
- package/src/scheduling/accessor.ts +65 -23
- package/src/scheduling/availability.test.ts +320 -0
- package/src/scheduling/availability.ts +251 -0
- package/src/scheduling/cancel.test.ts +550 -0
- package/src/scheduling/cancel.ts +254 -0
- package/src/scheduling/cluster-stock.test.ts +31 -0
- package/src/scheduling/cluster-stock.ts +15 -0
- package/src/scheduling/energy.ts +47 -0
- package/src/scheduling/epoch.ts +1 -0
- package/src/scheduling/idle-resolve.ts +45 -0
- package/src/scheduling/jobs.test.ts +448 -0
- package/src/scheduling/jobs.ts +343 -0
- package/src/scheduling/lane-core.ts +128 -0
- package/src/scheduling/lanes.test.ts +250 -0
- package/src/scheduling/lanes.ts +279 -0
- package/src/scheduling/projection.ts +265 -149
- package/src/scheduling/schedule.test.ts +163 -0
- package/src/scheduling/schedule.ts +282 -116
- package/src/scheduling/task-cargo.test.ts +110 -0
- package/src/scheduling/task-cargo.ts +62 -0
- package/src/scheduling/task-effects.test.ts +35 -0
- package/src/scheduling/task-effects.ts +34 -0
- package/src/scheduling/unwrap.test.ts +86 -0
- package/src/scheduling/unwrap.ts +190 -0
- package/src/shipload.ts +63 -4
- package/src/subscriptions/manager.cluster.test.ts +51 -0
- package/src/subscriptions/manager.ts +245 -172
- package/src/subscriptions/mappers.ts +5 -8
- package/src/subscriptions/types.ts +28 -3
- package/src/testing/catalog-hash.ts +19 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/projection-parity.ts +167 -0
- package/src/travel/reach.test.ts +86 -0
- package/src/travel/reach.ts +34 -0
- package/src/travel/route-build.test.ts +63 -0
- package/src/travel/route-build.ts +60 -0
- package/src/travel/route-planner.ts +288 -0
- package/src/travel/route-simulator.ts +170 -0
- package/src/travel/travel.ts +190 -123
- package/src/types/capabilities.ts +37 -8
- package/src/types/entity.ts +3 -3
- package/src/types/index.ts +0 -1
- package/src/types.ts +81 -23
- package/src/utils/cargo.test.ts +14 -0
- package/src/utils/cargo.ts +2 -0
- package/src/utils/display-name.ts +70 -0
- package/src/utils/hash.ts +22 -2
- package/src/utils/system.ts +24 -5
- package/src/capabilities/loading.ts +0 -8
- package/src/entities/container.ts +0 -123
- package/src/entities/ship-deploy.ts +0 -295
- package/src/entities/ship.ts +0 -221
- package/src/entities/warehouse.ts +0 -127
- package/src/types/entity-traits.ts +0 -69
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import {describe, expect, it} from 'bun:test'
|
|
2
|
+
import type {OrderedTask} from './schedule'
|
|
3
|
+
import {
|
|
4
|
+
buildJobCancelRoute,
|
|
5
|
+
jobCancelRoute,
|
|
6
|
+
jobCancellationBlockReason,
|
|
7
|
+
jobCancellable,
|
|
8
|
+
jobDropoffTask,
|
|
9
|
+
jobStatus,
|
|
10
|
+
jobStatusLabel,
|
|
11
|
+
pickupsInFlight,
|
|
12
|
+
splitJobCargo,
|
|
13
|
+
workshopAvailability,
|
|
14
|
+
type JobWindow,
|
|
15
|
+
} from './jobs'
|
|
16
|
+
|
|
17
|
+
const at = (s: string) => new Date(s)
|
|
18
|
+
|
|
19
|
+
const INPUTS = [{item_id: 101, stats: '413333752', quantity: 10}] as never
|
|
20
|
+
const FULL_INPUT = {
|
|
21
|
+
item_id: 101,
|
|
22
|
+
stats: '413333752',
|
|
23
|
+
modules: [{type: 1, installed: {item_id: 301, stats: '9'}}],
|
|
24
|
+
quantity: 10,
|
|
25
|
+
entity_id: '77',
|
|
26
|
+
}
|
|
27
|
+
const FULL_INPUTS = [FULL_INPUT] as never
|
|
28
|
+
|
|
29
|
+
function task(over: {
|
|
30
|
+
type: number
|
|
31
|
+
building: number
|
|
32
|
+
cargo?: unknown
|
|
33
|
+
startsAt: Date
|
|
34
|
+
completesAt: Date
|
|
35
|
+
}): OrderedTask {
|
|
36
|
+
return {
|
|
37
|
+
laneKey: 1,
|
|
38
|
+
taskIndex: 0,
|
|
39
|
+
startsAt: over.startsAt,
|
|
40
|
+
completesAt: over.completesAt,
|
|
41
|
+
task: {
|
|
42
|
+
type: over.type,
|
|
43
|
+
subject: {entity_id: over.building},
|
|
44
|
+
cargo: over.cargo ?? INPUTS,
|
|
45
|
+
},
|
|
46
|
+
} as unknown as OrderedTask
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('jobStatus', () => {
|
|
50
|
+
const job = {
|
|
51
|
+
startsAt: at('2026-07-26T10:00:00Z'),
|
|
52
|
+
completesAt: at('2026-07-26T11:00:00Z'),
|
|
53
|
+
deposited: true,
|
|
54
|
+
quantity: 1,
|
|
55
|
+
building: 42,
|
|
56
|
+
inputs: INPUTS,
|
|
57
|
+
}
|
|
58
|
+
const inFlight = {...job, deposited: false}
|
|
59
|
+
|
|
60
|
+
it('is queued once deposited and before startsAt', () => {
|
|
61
|
+
expect(jobStatus(job, at('2026-07-26T09:59:59Z'))).toBe('queued')
|
|
62
|
+
})
|
|
63
|
+
it('is crafting between startsAt and completesAt', () => {
|
|
64
|
+
expect(jobStatus(job, at('2026-07-26T10:30:00Z'))).toBe('crafting')
|
|
65
|
+
})
|
|
66
|
+
it('is ready at or after completesAt', () => {
|
|
67
|
+
expect(jobStatus(job, at('2026-07-26T11:00:00Z'))).toBe('ready')
|
|
68
|
+
expect(jobStatus(job, at('2026-07-26T12:00:00Z'))).toBe('ready')
|
|
69
|
+
})
|
|
70
|
+
it('is ready when the job was cancelled and holds its inputs', () => {
|
|
71
|
+
expect(jobStatus({...job, quantity: 0}, at('2026-07-26T09:00:00Z'))).toBe('ready')
|
|
72
|
+
})
|
|
73
|
+
it('is booked while the matching Drop-off task has not started', () => {
|
|
74
|
+
const tasks = [
|
|
75
|
+
task({
|
|
76
|
+
type: 21,
|
|
77
|
+
building: 42,
|
|
78
|
+
startsAt: at('2026-07-26T09:30:00Z'),
|
|
79
|
+
completesAt: at('2026-07-26T09:50:00Z'),
|
|
80
|
+
}),
|
|
81
|
+
]
|
|
82
|
+
expect(jobStatus(inFlight, at('2026-07-26T09:00:00Z'), tasks)).toBe('booked')
|
|
83
|
+
})
|
|
84
|
+
it('is dropping off once the Drop-off task has started', () => {
|
|
85
|
+
const tasks = [
|
|
86
|
+
task({
|
|
87
|
+
type: 21,
|
|
88
|
+
building: 42,
|
|
89
|
+
startsAt: at('2026-07-26T09:30:00Z'),
|
|
90
|
+
completesAt: at('2026-07-26T09:50:00Z'),
|
|
91
|
+
}),
|
|
92
|
+
]
|
|
93
|
+
expect(jobStatus(inFlight, at('2026-07-26T09:30:00Z'), tasks)).toBe('dropping')
|
|
94
|
+
})
|
|
95
|
+
it('matches the Drop-off by building and cargo, not by any deposit task', () => {
|
|
96
|
+
const tasks = [
|
|
97
|
+
task({
|
|
98
|
+
type: 21,
|
|
99
|
+
building: 42,
|
|
100
|
+
cargo: [{item_id: 999, stats: '1', quantity: 1}],
|
|
101
|
+
startsAt: at('2026-07-26T09:00:00Z'),
|
|
102
|
+
completesAt: at('2026-07-26T09:10:00Z'),
|
|
103
|
+
}),
|
|
104
|
+
task({
|
|
105
|
+
type: 21,
|
|
106
|
+
building: 42,
|
|
107
|
+
startsAt: at('2026-07-26T09:10:00Z'),
|
|
108
|
+
completesAt: at('2026-07-26T09:20:00Z'),
|
|
109
|
+
}),
|
|
110
|
+
]
|
|
111
|
+
expect(jobStatus(inFlight, at('2026-07-26T09:05:00Z'), tasks)).toBe('booked')
|
|
112
|
+
})
|
|
113
|
+
it('finds the Drop-off task the card reads its landing time from', () => {
|
|
114
|
+
const dropoff = task({
|
|
115
|
+
type: 21,
|
|
116
|
+
building: 42,
|
|
117
|
+
startsAt: at('2026-07-26T09:30:00Z'),
|
|
118
|
+
completesAt: at('2026-07-26T09:50:00Z'),
|
|
119
|
+
})
|
|
120
|
+
const other = task({
|
|
121
|
+
type: 22,
|
|
122
|
+
building: 42,
|
|
123
|
+
startsAt: at('2026-07-26T09:00:00Z'),
|
|
124
|
+
completesAt: at('2026-07-26T09:10:00Z'),
|
|
125
|
+
})
|
|
126
|
+
expect(jobDropoffTask(inFlight, [other, dropoff])).toBe(dropoff)
|
|
127
|
+
expect(jobDropoffTask(inFlight, [other])).toBeUndefined()
|
|
128
|
+
expect(jobDropoffTask(inFlight, undefined)).toBeUndefined()
|
|
129
|
+
})
|
|
130
|
+
it('uses the authoritative arrival to select one of two identical Drop-offs', () => {
|
|
131
|
+
const first = task({
|
|
132
|
+
type: 21,
|
|
133
|
+
building: 42,
|
|
134
|
+
startsAt: at('2026-07-26T09:10:00Z'),
|
|
135
|
+
completesAt: at('2026-07-26T09:20:00Z'),
|
|
136
|
+
})
|
|
137
|
+
const second = {
|
|
138
|
+
...task({
|
|
139
|
+
type: 21,
|
|
140
|
+
building: 42,
|
|
141
|
+
startsAt: at('2026-07-26T09:20:00Z'),
|
|
142
|
+
completesAt: at('2026-07-26T09:30:00Z'),
|
|
143
|
+
}),
|
|
144
|
+
taskIndex: 1,
|
|
145
|
+
}
|
|
146
|
+
expect(jobDropoffTask({...inFlight, arrivesAt: second.completesAt}, [first, second])).toBe(
|
|
147
|
+
second
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
it('matches modules and entity identity and refuses an ambiguous match', () => {
|
|
151
|
+
const exact = task({
|
|
152
|
+
type: 21,
|
|
153
|
+
building: 42,
|
|
154
|
+
cargo: FULL_INPUTS,
|
|
155
|
+
startsAt: at('2026-07-26T09:10:00Z'),
|
|
156
|
+
completesAt: at('2026-07-26T09:20:00Z'),
|
|
157
|
+
})
|
|
158
|
+
const differentEntity = task({
|
|
159
|
+
type: 21,
|
|
160
|
+
building: 42,
|
|
161
|
+
cargo: [{...FULL_INPUT, entity_id: '78'}],
|
|
162
|
+
startsAt: exact.startsAt,
|
|
163
|
+
completesAt: exact.completesAt,
|
|
164
|
+
})
|
|
165
|
+
expect(jobDropoffTask({...inFlight, inputs: FULL_INPUTS}, [differentEntity, exact])).toBe(
|
|
166
|
+
exact
|
|
167
|
+
)
|
|
168
|
+
expect(
|
|
169
|
+
jobDropoffTask({...inFlight, inputs: FULL_INPUTS}, [exact, {...exact}])
|
|
170
|
+
).toBeUndefined()
|
|
171
|
+
})
|
|
172
|
+
it('normalizes bare module layouts and absent entity identity like the contract', () => {
|
|
173
|
+
const bare = task({
|
|
174
|
+
type: 21,
|
|
175
|
+
building: 42,
|
|
176
|
+
cargo: [{item_id: 101, stats: '413333752', modules: [], quantity: 10}],
|
|
177
|
+
startsAt: at('2026-07-26T09:10:00Z'),
|
|
178
|
+
completesAt: at('2026-07-26T09:20:00Z'),
|
|
179
|
+
})
|
|
180
|
+
const padded = [
|
|
181
|
+
{
|
|
182
|
+
item_id: 101,
|
|
183
|
+
stats: '413333752',
|
|
184
|
+
modules: [{type: 6, installed: undefined}],
|
|
185
|
+
quantity: 10,
|
|
186
|
+
entity_id: 0,
|
|
187
|
+
},
|
|
188
|
+
] as never
|
|
189
|
+
expect(jobDropoffTask({...inFlight, inputs: padded}, [bare])).toBe(bare)
|
|
190
|
+
})
|
|
191
|
+
it('reads an undeposited row as dropping off when no schedule is given', () => {
|
|
192
|
+
expect(jobStatus(inFlight, at('2026-07-26T09:00:00Z'))).toBe('dropping')
|
|
193
|
+
})
|
|
194
|
+
it('labels every phase', () => {
|
|
195
|
+
expect(jobStatusLabel('booked')).toBe('Booked')
|
|
196
|
+
expect(jobStatusLabel('dropping')).toBe('Dropping off')
|
|
197
|
+
expect(jobStatusLabel('queued')).toBe('Queued')
|
|
198
|
+
expect(jobStatusLabel('crafting')).toBe('Crafting')
|
|
199
|
+
expect(jobStatusLabel('ready')).toBe('Ready for Pickup')
|
|
200
|
+
expect(jobStatusLabel('pickingup')).toBe('Picking up')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('jobCancelRoute', () => {
|
|
205
|
+
const job = {
|
|
206
|
+
id: 7,
|
|
207
|
+
shipId: 5,
|
|
208
|
+
startsAt: at('2026-07-26T10:00:00Z'),
|
|
209
|
+
completesAt: at('2026-07-26T11:00:00Z'),
|
|
210
|
+
deposited: true,
|
|
211
|
+
quantity: 1,
|
|
212
|
+
building: 42,
|
|
213
|
+
inputs: INPUTS,
|
|
214
|
+
}
|
|
215
|
+
const inFlight = {...job, deposited: false}
|
|
216
|
+
const dropoff = task({
|
|
217
|
+
type: 21,
|
|
218
|
+
building: 42,
|
|
219
|
+
startsAt: at('2026-07-26T09:30:00Z'),
|
|
220
|
+
completesAt: at('2026-07-26T09:50:00Z'),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('cancels a Booked job through the ship, from the Drop-off to the lane tail', () => {
|
|
224
|
+
const later = {
|
|
225
|
+
...dropoff,
|
|
226
|
+
taskIndex: 1,
|
|
227
|
+
task: {...dropoff.task, type: 22},
|
|
228
|
+
} as unknown as OrderedTask
|
|
229
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'), [dropoff, later])
|
|
230
|
+
expect(route).toEqual({kind: 'dropoff', shipId: 5, laneKey: 1, count: 2})
|
|
231
|
+
})
|
|
232
|
+
it('cancels from the selected identical Drop-off and leaves the earlier booking intact', () => {
|
|
233
|
+
const first = {...dropoff, completesAt: at('2026-07-26T09:20:00Z')}
|
|
234
|
+
const second = {
|
|
235
|
+
...dropoff,
|
|
236
|
+
taskIndex: 1,
|
|
237
|
+
startsAt: at('2026-07-26T09:20:00Z'),
|
|
238
|
+
completesAt: at('2026-07-26T09:30:00Z'),
|
|
239
|
+
}
|
|
240
|
+
expect(
|
|
241
|
+
jobCancelRoute(
|
|
242
|
+
{...inFlight, arrivesAt: second.completesAt},
|
|
243
|
+
at('2026-07-26T09:00:00Z'),
|
|
244
|
+
[first, second]
|
|
245
|
+
)
|
|
246
|
+
).toEqual({kind: 'dropoff', shipId: 5, laneKey: 1, count: 1})
|
|
247
|
+
})
|
|
248
|
+
it('cancels a Dropping off job through the ship while the transfer is in flight', () => {
|
|
249
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T09:40:00Z'), [dropoff])
|
|
250
|
+
expect(route).toEqual({kind: 'dropoff', shipId: 5, laneKey: 1, count: 1})
|
|
251
|
+
})
|
|
252
|
+
it('counts only the Drop-off lane, not tasks on other lanes', () => {
|
|
253
|
+
const mobility = {
|
|
254
|
+
...dropoff,
|
|
255
|
+
laneKey: 0,
|
|
256
|
+
taskIndex: 0,
|
|
257
|
+
task: {...dropoff.task, type: 1},
|
|
258
|
+
} as unknown as OrderedTask
|
|
259
|
+
const route = jobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'), [mobility, dropoff])
|
|
260
|
+
expect(route).toEqual({kind: 'dropoff', shipId: 5, laneKey: 1, count: 1})
|
|
261
|
+
})
|
|
262
|
+
it('cancels a Queued job through cancelcraft', () => {
|
|
263
|
+
expect(jobCancelRoute(job, at('2026-07-26T09:59:59Z'))).toEqual({kind: 'craft', jobId: 7})
|
|
264
|
+
})
|
|
265
|
+
it('refuses only deposited positive-quantity output-only legacy jobs', () => {
|
|
266
|
+
const legacy = {...job, inputs: []}
|
|
267
|
+
expect(jobCancellationBlockReason(legacy)).toBe('legacy-inputs-unavailable')
|
|
268
|
+
expect(jobCancelRoute(legacy, at('2026-07-26T09:59:59Z'))).toBeNull()
|
|
269
|
+
expect(jobCancellationBlockReason({...legacy, deposited: false})).toBeNull()
|
|
270
|
+
expect(jobCancellationBlockReason({...legacy, quantity: 0})).toBeNull()
|
|
271
|
+
expect(jobCancellationBlockReason({...legacy, inputs: undefined})).toBeNull()
|
|
272
|
+
expect(jobCancellationBlockReason({...legacy, inputs: [FULL_INPUTS[0]]})).toBeNull()
|
|
273
|
+
})
|
|
274
|
+
it('refuses an existing exact-arrival tie rather than choosing either Drop-off', () => {
|
|
275
|
+
const tied = {...inFlight, arrivesAt: dropoff.completesAt}
|
|
276
|
+
expect(jobCancellationBlockReason(tied, [dropoff, {...dropoff}])).toBe('ambiguous-dropoff')
|
|
277
|
+
expect(jobCancelRoute(tied, at('2026-07-26T09:00:00Z'), [dropoff, {...dropoff}])).toBeNull()
|
|
278
|
+
})
|
|
279
|
+
it('has no route once crafting has started or the job is ready', () => {
|
|
280
|
+
expect(jobCancelRoute(job, at('2026-07-26T10:00:00Z'))).toBeNull()
|
|
281
|
+
expect(jobCancelRoute(job, at('2026-07-26T11:00:00Z'))).toBeNull()
|
|
282
|
+
expect(jobCancelRoute({...job, quantity: 0}, at('2026-07-26T09:00:00Z'))).toBeNull()
|
|
283
|
+
})
|
|
284
|
+
it('has no route for a landed Drop-off that has not resolved yet', () => {
|
|
285
|
+
expect(jobCancelRoute(inFlight, at('2026-07-26T09:50:00Z'), [dropoff])).toBeNull()
|
|
286
|
+
})
|
|
287
|
+
it('has no route for an undeposited job without the ship schedule', () => {
|
|
288
|
+
expect(jobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'))).toBeNull()
|
|
289
|
+
expect(
|
|
290
|
+
jobCancelRoute({...inFlight, shipId: undefined}, at('2026-07-26T09:00:00Z'), [dropoff])
|
|
291
|
+
).toBeNull()
|
|
292
|
+
})
|
|
293
|
+
it('names the three cancellable phases', () => {
|
|
294
|
+
expect(jobCancellable('booked')).toBe(true)
|
|
295
|
+
expect(jobCancellable('dropping')).toBe(true)
|
|
296
|
+
expect(jobCancellable('queued')).toBe(true)
|
|
297
|
+
expect(jobCancellable('crafting')).toBe(false)
|
|
298
|
+
expect(jobCancellable('ready')).toBe(false)
|
|
299
|
+
expect(jobCancellable('pickingup')).toBe(false)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('buildJobCancelRoute', () => {
|
|
304
|
+
const job = {
|
|
305
|
+
id: 3,
|
|
306
|
+
targetId: 9,
|
|
307
|
+
startsAt: at('2026-07-26T10:00:00Z'),
|
|
308
|
+
completesAt: at('2026-07-26T11:00:00Z'),
|
|
309
|
+
deposited: true,
|
|
310
|
+
building: 42,
|
|
311
|
+
inputs: INPUTS,
|
|
312
|
+
}
|
|
313
|
+
const inFlight = {...job, deposited: false}
|
|
314
|
+
const dropoff = task({
|
|
315
|
+
type: 21,
|
|
316
|
+
building: 42,
|
|
317
|
+
startsAt: at('2026-07-26T09:30:00Z'),
|
|
318
|
+
completesAt: at('2026-07-26T09:50:00Z'),
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('cancels a Booked or Dropping off build through the target, from the Drop-off to the lane tail', () => {
|
|
322
|
+
expect(buildJobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'), [dropoff])).toEqual({
|
|
323
|
+
kind: 'dropoff',
|
|
324
|
+
shipId: 9,
|
|
325
|
+
laneKey: 1,
|
|
326
|
+
count: 1,
|
|
327
|
+
})
|
|
328
|
+
expect(buildJobCancelRoute(inFlight, at('2026-07-26T09:40:00Z'), [dropoff])).toEqual({
|
|
329
|
+
kind: 'dropoff',
|
|
330
|
+
shipId: 9,
|
|
331
|
+
laneKey: 1,
|
|
332
|
+
count: 1,
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
it('cancels a Queued build through cancelbuild', () => {
|
|
336
|
+
expect(buildJobCancelRoute(job, at('2026-07-26T09:59:59Z'))).toEqual({
|
|
337
|
+
kind: 'build',
|
|
338
|
+
jobId: 3,
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
it('has no route once the build has started or finished', () => {
|
|
342
|
+
expect(buildJobCancelRoute(job, at('2026-07-26T10:00:00Z'))).toBeNull()
|
|
343
|
+
expect(buildJobCancelRoute(job, at('2026-07-26T11:00:00Z'))).toBeNull()
|
|
344
|
+
})
|
|
345
|
+
it('has no route for an undeposited build without the target schedule', () => {
|
|
346
|
+
expect(buildJobCancelRoute(inFlight, at('2026-07-26T09:00:00Z'))).toBeNull()
|
|
347
|
+
expect(
|
|
348
|
+
buildJobCancelRoute({...inFlight, targetId: undefined}, at('2026-07-26T09:00:00Z'), [
|
|
349
|
+
dropoff,
|
|
350
|
+
])
|
|
351
|
+
).toBeNull()
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
describe('pickupsInFlight', () => {
|
|
356
|
+
it('lists the civic withdraw tasks bound for a building', () => {
|
|
357
|
+
const tasks = [
|
|
358
|
+
task({
|
|
359
|
+
type: 22,
|
|
360
|
+
building: 42,
|
|
361
|
+
cargo: [{item_id: 10001, stats: '5', quantity: 1}],
|
|
362
|
+
startsAt: at('2026-07-26T09:00:00Z'),
|
|
363
|
+
completesAt: at('2026-07-26T09:10:00Z'),
|
|
364
|
+
}),
|
|
365
|
+
task({
|
|
366
|
+
type: 21,
|
|
367
|
+
building: 42,
|
|
368
|
+
startsAt: at('2026-07-26T09:10:00Z'),
|
|
369
|
+
completesAt: at('2026-07-26T09:20:00Z'),
|
|
370
|
+
}),
|
|
371
|
+
task({
|
|
372
|
+
type: 22,
|
|
373
|
+
building: 7,
|
|
374
|
+
startsAt: at('2026-07-26T09:20:00Z'),
|
|
375
|
+
completesAt: at('2026-07-26T09:30:00Z'),
|
|
376
|
+
}),
|
|
377
|
+
]
|
|
378
|
+
const pickups = pickupsInFlight(tasks, 42)
|
|
379
|
+
expect(pickups).toHaveLength(1)
|
|
380
|
+
expect(pickups[0].building).toBe(42)
|
|
381
|
+
expect(pickups[0].completesAt).toEqual(at('2026-07-26T09:10:00Z'))
|
|
382
|
+
expect(pickups[0].cargo).toEqual([{item_id: 10001, stats: '5', quantity: 1}] as never)
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
describe('splitJobCargo', () => {
|
|
387
|
+
it('reads an undeposited row as inputs with no output', () => {
|
|
388
|
+
const cargo = [{n: 'a'}, {n: 'b'}] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
389
|
+
const {output, inputs} = splitJobCargo(cargo, false, 1)
|
|
390
|
+
expect(output).toBeNull()
|
|
391
|
+
expect(inputs).toEqual([{n: 'a'}, {n: 'b'}] as never)
|
|
392
|
+
})
|
|
393
|
+
it('reads a deposited row as inputs followed by the output tail', () => {
|
|
394
|
+
const cargo = [{n: 'a'}, {n: 'out'}] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
395
|
+
const {output, inputs} = splitJobCargo(cargo, true, 1)
|
|
396
|
+
expect(output).toEqual({n: 'out'} as never)
|
|
397
|
+
expect(inputs).toEqual([{n: 'a'}] as never)
|
|
398
|
+
})
|
|
399
|
+
it('reads a legacy deposited row holding only its output', () => {
|
|
400
|
+
const cargo = [{n: 'out'}] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
401
|
+
const {output, inputs} = splitJobCargo(cargo, true, 1)
|
|
402
|
+
expect(output).toEqual({n: 'out'} as never)
|
|
403
|
+
expect(inputs).toEqual([] as never)
|
|
404
|
+
})
|
|
405
|
+
it('reads a cancelled row as inputs held for pickup', () => {
|
|
406
|
+
const cargo = [{n: 'a'}, {n: 'b'}] as unknown as Parameters<typeof splitJobCargo>[0]
|
|
407
|
+
const {output, inputs} = splitJobCargo(cargo, true, 0)
|
|
408
|
+
expect(output).toBeNull()
|
|
409
|
+
expect(inputs).toEqual([{n: 'a'}, {n: 'b'}] as never)
|
|
410
|
+
})
|
|
411
|
+
it('returns null output for empty cargo', () => {
|
|
412
|
+
expect(splitJobCargo([], true, 1)).toEqual({output: null, inputs: []})
|
|
413
|
+
expect(splitJobCargo([], false, 1)).toEqual({output: null, inputs: []})
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
describe('workshopAvailability', () => {
|
|
418
|
+
const win = (socket: number, s: string, e: string): JobWindow => ({
|
|
419
|
+
id: 1,
|
|
420
|
+
socket,
|
|
421
|
+
owner: 'x',
|
|
422
|
+
startsAt: at(s),
|
|
423
|
+
completesAt: at(e),
|
|
424
|
+
recipeId: 1,
|
|
425
|
+
quantity: 1,
|
|
426
|
+
deposited: true,
|
|
427
|
+
})
|
|
428
|
+
const now = at('2026-07-26T10:00:00Z')
|
|
429
|
+
|
|
430
|
+
it('counts sockets with no live window as open now', () => {
|
|
431
|
+
const jobs = [win(0, '2026-07-26T09:00:00Z', '2026-07-26T11:00:00Z')]
|
|
432
|
+
expect(workshopAvailability(jobs, 3, now)).toEqual({open: 2, nextOpeningAt: null})
|
|
433
|
+
})
|
|
434
|
+
it('reports the earliest tail when every socket is booked', () => {
|
|
435
|
+
const jobs = [
|
|
436
|
+
win(0, '2026-07-26T09:00:00Z', '2026-07-26T11:00:00Z'),
|
|
437
|
+
win(1, '2026-07-26T09:00:00Z', '2026-07-26T10:40:00Z'),
|
|
438
|
+
]
|
|
439
|
+
expect(workshopAvailability(jobs, 2, now)).toEqual({
|
|
440
|
+
open: 0,
|
|
441
|
+
nextOpeningAt: at('2026-07-26T10:40:00Z'),
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
it('ignores ended windows', () => {
|
|
445
|
+
const jobs = [win(0, '2026-07-26T08:00:00Z', '2026-07-26T09:00:00Z')]
|
|
446
|
+
expect(workshopAvailability(jobs, 1, now)).toEqual({open: 1, nextOpeningAt: null})
|
|
447
|
+
})
|
|
448
|
+
})
|