@roster-lock/match-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +11 -0
- package/dist/authentication.js +53 -0
- package/dist/authentication.js.map +1 -0
- package/dist/config/constants.js +12 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/findFile.js +32 -0
- package/dist/config/findFile.js.map +1 -0
- package/dist/config/index.js +29 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manage.js +41 -0
- package/dist/config/manage.js.map +1 -0
- package/dist/cors.js +20 -0
- package/dist/cors.js.map +1 -0
- package/dist/editor-support/index.js +247 -0
- package/dist/editor-support/index.js.map +1 -0
- package/dist/handle-room/version-1/file-routes.js +53 -0
- package/dist/handle-room/version-1/file-routes.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js +76 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js +218 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js +16 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js +370 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js +3 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js +95 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/index.js +18 -0
- package/dist/handle-room/version-1/globals/Room/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/room.js +3 -0
- package/dist/handle-room/version-1/globals/Room/room.js.map +1 -0
- package/dist/handle-room/version-1/globals/index.js +20 -0
- package/dist/handle-room/version-1/globals/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/types.js +3 -0
- package/dist/handle-room/version-1/globals/types.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js +83 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/index.js +35 -0
- package/dist/handle-room/version-1/handleDownloads/index.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/types.js +3 -0
- package/dist/handle-room/version-1/handleDownloads/types.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js +21 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js +12 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js.map +1 -0
- package/dist/handle-room/version-1/index.js +28 -0
- package/dist/handle-room/version-1/index.js.map +1 -0
- package/dist/handle-room/version-1/piece-sort.js +71 -0
- package/dist/handle-room/version-1/piece-sort.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js +89 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js +19 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js +170 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js +28 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js.map +1 -0
- package/dist/handle-room/version-1/schema/count.js +16 -0
- package/dist/handle-room/version-1/schema/count.js.map +1 -0
- package/dist/handle-room/version-1/schema/lock.js +46 -0
- package/dist/handle-room/version-1/schema/lock.js.map +1 -0
- package/dist/handle-room/version-1/schema/selected.js +13 -0
- package/dist/handle-room/version-1/schema/selected.js.map +1 -0
- package/dist/handle-room/version-1/select.js +94 -0
- package/dist/handle-room/version-1/select.js.map +1 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +88 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/fs.js +32 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/http-router.js +49 -0
- package/dist/utils/http-router.js.map +1 -0
- package/dist/utils/pagination.js +23 -0
- package/dist/utils/pagination.js.map +1 -0
- package/dist/utils/websocket-router.js +15 -0
- package/dist/utils/websocket-router.js.map +1 -0
- package/package.json +66 -0
- package/src/authentication.ts +48 -0
- package/src/config/constants.ts +7 -0
- package/src/config/findFile.ts +34 -0
- package/src/config/index.ts +17 -0
- package/src/config/manage.ts +48 -0
- package/src/cors.ts +23 -0
- package/src/editor-support/index.ts +289 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/index.ts +109 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/isUpToDate.ts +78 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/downloadSingleAsset.ts +129 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/example.ts +116 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/index.ts +144 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/types.ts +32 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/validateAssetFile.ts +54 -0
- package/src/handle-room/version-0/piece-download/index.ts +5 -0
- package/src/handle-room/version-0/piece-selection/constants.ts +39 -0
- package/src/handle-room/version-0/piece-selection/handleRoomSelections.ts +202 -0
- package/src/handle-room/version-0/piece-selection/index.ts +31 -0
- package/src/handle-room/version-0/piece-selection/roomErrors.ts +25 -0
- package/src/handle-room/version-0/piece-selection/steps/encrypton.ts +70 -0
- package/src/handle-room/version-0/piece-selection/steps/final-selection.ts +72 -0
- package/src/handle-room/version-0/piece-selection/steps/player-selection.ts +100 -0
- package/src/handle-room/version-1/constants.ts +44 -0
- package/src/handle-room/version-1/file-routes.ts +68 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.ts +83 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.ts +317 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.ts +31 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.ts +13 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.ts +547 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/types.ts +6 -0
- package/src/handle-room/version-1/globals/FolderDB/index.ts +3 -0
- package/src/handle-room/version-1/globals/FolderDB/types.ts +72 -0
- package/src/handle-room/version-1/globals/Room/WebsocketRoom.ts +118 -0
- package/src/handle-room/version-1/globals/Room/index.ts +3 -0
- package/src/handle-room/version-1/globals/Room/room.ts +8 -0
- package/src/handle-room/version-1/globals/index.ts +4 -0
- package/src/handle-room/version-1/globals/types.ts +7 -0
- package/src/handle-room/version-1/handleDownloads/DownloadTracker.ts +98 -0
- package/src/handle-room/version-1/handleDownloads/index.ts +41 -0
- package/src/handle-room/version-1/handleDownloads/types.ts +9 -0
- package/src/handle-room/version-1/handleRoomSelections/encryption.ts +29 -0
- package/src/handle-room/version-1/handleRoomSelections/random.ts +9 -0
- package/src/handle-room/version-1/index.ts +33 -0
- package/src/handle-room/version-1/piece-sort.ts +96 -0
- package/src/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.ts +125 -0
- package/src/handle-room/version-1/room-handler-bridge/http-handler.ts +16 -0
- package/src/handle-room/version-1/room-handler-bridge/server.pseudo.ts +85 -0
- package/src/handle-room/version-1/room-handler-bridge/steps.ts +255 -0
- package/src/handle-room/version-1/room-handler-bridge/ws-handler.ts +30 -0
- package/src/handle-room/version-1/room-handler-state/RoomState.ts +132 -0
- package/src/handle-room/version-1/room-handler-state/bind-to-websocket.ts +93 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomSelections.ts +71 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomWithErrors.ts +55 -0
- package/src/handle-room/version-1/room-handler-state/roomErrors.ts +35 -0
- package/src/handle-room/version-1/room-handler-state/user-selection.ts +98 -0
- package/src/handle-room/version-1/schema/count.ts +13 -0
- package/src/handle-room/version-1/schema/lock.ts +47 -0
- package/src/handle-room/version-1/schema/selected.ts +14 -0
- package/src/handle-room/version-1/select.ts +130 -0
- package/src/handle-room/version-1/validateUnknown.ts +11 -0
- package/src/index.old.ts +71 -0
- package/src/index.ts +231 -0
- package/src/server.ts +85 -0
- package/src/utils/fs.ts +43 -0
- package/src/utils/http-router.ts +55 -0
- package/src/utils/pagination.ts +17 -0
- package/src/utils/websocket-router.ts +23 -0
- package/tests/integration/editor-pieces-search.test.ts +147 -0
- package/tests/integration/game-complete.test.ts +72 -0
- package/tests/integration/handle-full-selection.test.ts +143 -0
- package/tests/integration/helpers/bridge.ts +22 -0
- package/tests/integration/helpers/fakeFolderDB.ts +37 -0
- package/tests/integration/helpers/fakePluginRuntime.ts +46 -0
- package/tests/integration/helpers/lockConfig.ts +71 -0
- package/tests/integration/helpers/make-tar-gz.ts +41 -0
- package/tests/integration/helpers/piece.ts +98 -0
- package/tests/integration/helpers/plugin-dir.ts +26 -0
- package/tests/integration/helpers/room.ts +42 -0
- package/tests/integration/helpers/server.ts +51 -0
- package/tests/integration/helpers/validLockConfig.ts +80 -0
- package/tests/integration/piece-download-and-list.test.ts +194 -0
- package/tests/integration/piece-ensure-ws.test.ts +256 -0
- package/tests/integration/piece-files.test.ts +139 -0
- package/tests/integration/piece-sort-list.test.ts +113 -0
- package/tests/integration/protocol.test.ts +129 -0
- package/tests/integration/root-and-auth.test.ts +100 -0
- package/tests/integration/sync-dl.test.ts +62 -0
- package/tests/tsconfig.json +14 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
+
import { PluginManager } from "@roster-lock/plugin-runtime";
|
|
3
|
+
import { bindStepsToBridge } from "../../src/handle-room/version-1/room-handler-bridge/steps";
|
|
4
|
+
import { wireBridgePair } from "./helpers/bridge";
|
|
5
|
+
import { driveRoomProtocol } from "./helpers/room";
|
|
6
|
+
import { FakeFolderDB } from "./helpers/fakeFolderDB";
|
|
7
|
+
import { makeLockConfig, makeHeroSelection } from "./helpers/lockConfig";
|
|
8
|
+
import { createFixturePluginDir } from "./helpers/plugin-dir";
|
|
9
|
+
|
|
10
|
+
const MOST_USED_LOCALLY = "@roster-lock/piece-selection-sort-most-used-locally";
|
|
11
|
+
|
|
12
|
+
describe("bindStepsToBridge: handleFullSelection hook", () => {
|
|
13
|
+
const cleanups: Array<() => Promise<void> | void> = [];
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
while (cleanups.length) await cleanups.pop()!();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("fires handleFullSelection against every installed piece-selection-sort plugin once the room agrees on a final selection", async () => {
|
|
19
|
+
const fixture = await createFixturePluginDir([MOST_USED_LOCALLY]);
|
|
20
|
+
cleanups.push(fixture.cleanup);
|
|
21
|
+
const pluginRuntime = await PluginManager.create(fixture.pluginDir);
|
|
22
|
+
|
|
23
|
+
const lockConfig = makeLockConfig();
|
|
24
|
+
const machines = [{ publicKey: "pk-a", playerCount: 1 }, { publicKey: "pk-b", playerCount: 1 }];
|
|
25
|
+
|
|
26
|
+
const pairA = wireBridgePair();
|
|
27
|
+
const pairB = wireBridgePair();
|
|
28
|
+
|
|
29
|
+
const resultAPromise = bindStepsToBridge({
|
|
30
|
+
bridge: pairA.agentSide,
|
|
31
|
+
fileDB: new FakeFolderDB(),
|
|
32
|
+
pluginRuntime,
|
|
33
|
+
machines,
|
|
34
|
+
ownMachinePublicKey: "pk-a",
|
|
35
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
36
|
+
lockConfig,
|
|
37
|
+
gameControlledSelections: {},
|
|
38
|
+
});
|
|
39
|
+
const resultBPromise = bindStepsToBridge({
|
|
40
|
+
bridge: pairB.agentSide,
|
|
41
|
+
fileDB: new FakeFolderDB(),
|
|
42
|
+
pluginRuntime,
|
|
43
|
+
machines,
|
|
44
|
+
ownMachinePublicKey: "pk-b",
|
|
45
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
46
|
+
lockConfig,
|
|
47
|
+
gameControlledSelections: {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await driveRoomProtocol([
|
|
51
|
+
{ bridge: pairA.roomSide, publicKey: "pk-a" },
|
|
52
|
+
{ bridge: pairB.roomSide, publicKey: "pk-b" },
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
await Promise.all([resultAPromise, resultBPromise]);
|
|
56
|
+
|
|
57
|
+
// The hook fires off a promise.then() on the room's overall promise, so
|
|
58
|
+
// it isn't awaited by bindStepsToBridge itself - both agents finishing
|
|
59
|
+
// above only proves the room succeeded, not that the hook has already
|
|
60
|
+
// run. Poll instead of asserting immediately.
|
|
61
|
+
// Both agents picked "hero-1" (logic hash "1.0.0" per makeLockConfig) -
|
|
62
|
+
// proves the hook actually ran with real decrypted userSelections, not
|
|
63
|
+
// just that the room protocol completed.
|
|
64
|
+
await vi.waitFor(async () => {
|
|
65
|
+
const ranked = await pluginRuntime.pieceSort.sortPieces(MOST_USED_LOCALLY, {
|
|
66
|
+
lockConfig, pieceType: "character",
|
|
67
|
+
});
|
|
68
|
+
expect(ranked).toEqual(["1.0.0"]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not fail the room when a piece-selection-sort plugin's handleFullSelection throws", async () => {
|
|
73
|
+
const fixture = await createFixturePluginDir([]);
|
|
74
|
+
cleanups.push(fixture.cleanup);
|
|
75
|
+
const pluginDir = fixture.pluginDir;
|
|
76
|
+
|
|
77
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
78
|
+
const { join } = await import("node:path");
|
|
79
|
+
const pkgDir = join(pluginDir, "node_modules", "broken-plugin");
|
|
80
|
+
await mkdir(pkgDir, { recursive: true });
|
|
81
|
+
await writeFile(join(pkgDir, "package.json"), JSON.stringify({
|
|
82
|
+
name: "broken-plugin", version: "1.0.0", main: "index.js",
|
|
83
|
+
"roster-lock": { pluginType: "piece-selection-sort", priority: 0 },
|
|
84
|
+
}));
|
|
85
|
+
await writeFile(join(pkgDir, "index.js"), `
|
|
86
|
+
module.exports.default = {
|
|
87
|
+
name: "broken-plugin",
|
|
88
|
+
publicInfo: { title: "Broken", description: "Always throws" },
|
|
89
|
+
async sortPieces() { throw new Error("boom"); },
|
|
90
|
+
async handleFullSelection() { throw new Error("boom"); },
|
|
91
|
+
async handleGameComplete() { throw new Error("boom"); },
|
|
92
|
+
};
|
|
93
|
+
`);
|
|
94
|
+
await writeFile(join(pluginDir, "plugins.json"), JSON.stringify({
|
|
95
|
+
plugins: [{ package: "broken-plugin", version: "1.0.0", type: "piece-selection-sort", priority: 0 }],
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
const pluginRuntime = await PluginManager.create(pluginDir);
|
|
99
|
+
const lockConfig = makeLockConfig();
|
|
100
|
+
const machines = [{ publicKey: "pk-a", playerCount: 1 }, { publicKey: "pk-b", playerCount: 1 }];
|
|
101
|
+
|
|
102
|
+
const pairA = wireBridgePair();
|
|
103
|
+
const pairB = wireBridgePair();
|
|
104
|
+
|
|
105
|
+
const resultAPromise = bindStepsToBridge({
|
|
106
|
+
bridge: pairA.agentSide,
|
|
107
|
+
fileDB: new FakeFolderDB(),
|
|
108
|
+
pluginRuntime,
|
|
109
|
+
machines,
|
|
110
|
+
ownMachinePublicKey: "pk-a",
|
|
111
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
112
|
+
lockConfig,
|
|
113
|
+
gameControlledSelections: {},
|
|
114
|
+
});
|
|
115
|
+
const resultBPromise = bindStepsToBridge({
|
|
116
|
+
bridge: pairB.agentSide,
|
|
117
|
+
fileDB: new FakeFolderDB(),
|
|
118
|
+
pluginRuntime,
|
|
119
|
+
machines,
|
|
120
|
+
ownMachinePublicKey: "pk-b",
|
|
121
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
122
|
+
lockConfig,
|
|
123
|
+
gameControlledSelections: {},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await driveRoomProtocol([
|
|
127
|
+
{ bridge: pairA.roomSide, publicKey: "pk-a" },
|
|
128
|
+
{ bridge: pairB.roomSide, publicKey: "pk-b" },
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const [resultA] = await Promise.all([resultAPromise, resultBPromise]);
|
|
132
|
+
expect(resultA.finalSelection.character).toBeTruthy();
|
|
133
|
+
|
|
134
|
+
// The hook (and its failure) happen off promise.then(), after the room
|
|
135
|
+
// above already resolved - wait for it to actually settle before this
|
|
136
|
+
// test ends, or its dataDir mkdir can still be in flight when afterEach's
|
|
137
|
+
// fixture.cleanup() starts deleting the same directory tree (ENOTEMPTY).
|
|
138
|
+
const { existsSync } = await import("node:fs");
|
|
139
|
+
await vi.waitFor(() => {
|
|
140
|
+
expect(existsSync(join(pluginDir, "data", "broken-plugin"))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MessageBridge, MessageBridgeMessage } from "@roster-lock/utils";
|
|
2
|
+
|
|
3
|
+
// bindStepsToBridge (steps.ts) is written against a single MessageBridge per
|
|
4
|
+
// side of a connection - in production that's a WebSocket to the relay
|
|
5
|
+
// server. There's no need for a real socket (or a relay server) to exercise
|
|
6
|
+
// it: two directly-wired MessageBridge instances, each calling the other's
|
|
7
|
+
// handleMessage synchronously, are exactly the same shape from steps.ts's
|
|
8
|
+
// point of view. "agentSide" is what bindStepsToBridge is given; "roomSide"
|
|
9
|
+
// is the handle a test uses to play the relay's part (see room.ts).
|
|
10
|
+
export function wireBridgePair() {
|
|
11
|
+
let agentSide!: MessageBridge;
|
|
12
|
+
let roomSide!: MessageBridge;
|
|
13
|
+
agentSide = new MessageBridge((message) => deliver(roomSide, message));
|
|
14
|
+
roomSide = new MessageBridge((message) => deliver(agentSide, message));
|
|
15
|
+
return { agentSide, roomSide };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function deliver(bridge: MessageBridge, message: MessageBridgeMessage) {
|
|
19
|
+
// Round-trips through JSON like a real socket would, so a value that
|
|
20
|
+
// can't survive serialization fails here instead of only over the wire.
|
|
21
|
+
void bridge.handleMessage(JSON.parse(JSON.stringify(message)));
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { RosterLockV1Config, RosterLockPiece } from "@roster-lock/types";
|
|
3
|
+
import { IFolderDB } from "../../../src/handle-room/version-1/globals/FolderDB";
|
|
4
|
+
import { ProgressHandlers } from "../../../src/handle-room/version-1/handleDownloads/types";
|
|
5
|
+
|
|
6
|
+
// handleDownloads/DownloadTracker's job is orchestration (required-piece
|
|
7
|
+
// recursion, dedup, results-map building) - the actual file transfer is
|
|
8
|
+
// @roster-lock/plugin-runtime's concern. This fake resolves instantly and
|
|
9
|
+
// just records what it was asked to fetch, so tests can assert on the
|
|
10
|
+
// orchestration without a real download source.
|
|
11
|
+
export class FakeFolderDB implements IFolderDB {
|
|
12
|
+
public calls: Array<{ pieceType: string, pieceId: string }> = [];
|
|
13
|
+
|
|
14
|
+
async ensurePieceExists(
|
|
15
|
+
_lockConfig: RosterLockV1Config["engine"],
|
|
16
|
+
pieceType: string,
|
|
17
|
+
selectedPiece: RosterLockPiece,
|
|
18
|
+
_progressHandlers: ProgressHandlers,
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
this.calls.push({ pieceType, pieceId: selectedPiece.id });
|
|
21
|
+
return `/fake/${pieceType}/${selectedPiece.id}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async *getFilesofAsset(): AsyncIterable<string> {}
|
|
25
|
+
|
|
26
|
+
async getPieceFileContents(): ReturnType<IFolderDB["getPieceFileContents"]> {
|
|
27
|
+
throw new Error("not implemented in FakeFolderDB");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listPieces(): ReturnType<IFolderDB["listPieces"]> {
|
|
31
|
+
throw new Error("not implemented in FakeFolderDB");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async searchPieces(): ReturnType<IFolderDB["searchPieces"]>{
|
|
35
|
+
throw new Error("not implemented in FakeFolderDB");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { IPluginManager, IPieceSort } from "@roster-lock/plugin-runtime";
|
|
2
|
+
|
|
3
|
+
export class FakePluginManager implements IPluginManager {
|
|
4
|
+
readonly pluginDir = "hello-world";
|
|
5
|
+
pieceSort = new FakePieceSort()
|
|
6
|
+
uninstallPlugin(): ReturnType<IPluginManager["uninstallPlugin"]>{
|
|
7
|
+
throw new Error("Not implemented");
|
|
8
|
+
}
|
|
9
|
+
setPluginPriority(): ReturnType<IPluginManager["setPluginPriority"]>{
|
|
10
|
+
throw new Error("Not implemented");
|
|
11
|
+
}
|
|
12
|
+
getPluginPackagesOfType(): ReturnType<IPluginManager["getPluginPackagesOfType"]>{
|
|
13
|
+
throw new Error("Not implemented");
|
|
14
|
+
}
|
|
15
|
+
getPluginFullOfType(): ReturnType<any>{
|
|
16
|
+
throw new Error("Not implemented");
|
|
17
|
+
}
|
|
18
|
+
installPlugin(): ReturnType<IPluginManager["installPlugin"]>{
|
|
19
|
+
throw new Error("Not Implemented");
|
|
20
|
+
}
|
|
21
|
+
updatePlugin(): ReturnType<IPluginManager["updatePlugin"]>{
|
|
22
|
+
throw new Error("Not Implemented");
|
|
23
|
+
}
|
|
24
|
+
downloadToFolder(): ReturnType<IPluginManager["downloadToFolder"]>{
|
|
25
|
+
throw new Error("Not Implemented");
|
|
26
|
+
}
|
|
27
|
+
runUntrustedScript(): ReturnType<IPluginManager["runUntrustedScript"]>{
|
|
28
|
+
throw new Error("Not Implemented");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class FakePieceSort implements IPieceSort {
|
|
33
|
+
constructor(){}
|
|
34
|
+
listAvailable(): ReturnType<IPieceSort["listAvailable"]>{
|
|
35
|
+
throw new Error("No implmented");
|
|
36
|
+
}
|
|
37
|
+
sortPieces(): ReturnType<IPieceSort["sortPieces"]>{
|
|
38
|
+
throw new Error("No implmented");
|
|
39
|
+
}
|
|
40
|
+
handleFullSelection(): ReturnType<IPieceSort["handleFullSelection"]>{
|
|
41
|
+
throw new Error("No implmented");
|
|
42
|
+
}
|
|
43
|
+
handleGameComplete(): ReturnType<IPieceSort["handleGameComplete"]>{
|
|
44
|
+
throw new Error("Not Implmented")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { RosterLockV1Config, SelectedPiece } from "@roster-lock/types";
|
|
2
|
+
|
|
3
|
+
// A minimal but real config exercising a "personal" selection (each user
|
|
4
|
+
// picks independently, no merge script needed) where the picked piece has
|
|
5
|
+
// a mandatory nested requirement - enough to drive runSelection's roster
|
|
6
|
+
// validation and handleDownloads' recursive/deduped download resolution
|
|
7
|
+
// without needing untrusted-script execution.
|
|
8
|
+
export function makeLockConfig(): RosterLockV1Config {
|
|
9
|
+
return {
|
|
10
|
+
configIdentity: { namespace: "roster-lock", purpose: "lock", version: 1 },
|
|
11
|
+
author: "test",
|
|
12
|
+
title: "Test Config",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
engine: {
|
|
15
|
+
name: "test-engine",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
pieceDefinitions: {
|
|
18
|
+
character: {
|
|
19
|
+
selectionStrategy: "personal",
|
|
20
|
+
requires: ["weapon"],
|
|
21
|
+
pathVariables: [],
|
|
22
|
+
assets: [{ name: "model", classification: "media", count: 1, glob: ["model.glb"] }],
|
|
23
|
+
},
|
|
24
|
+
weapon: {
|
|
25
|
+
selectionStrategy: "on demand",
|
|
26
|
+
requires: [],
|
|
27
|
+
pathVariables: [],
|
|
28
|
+
assets: [{ name: "model", classification: "media", count: 1, glob: ["model.glb"] }],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
rosters: {
|
|
33
|
+
character: [{
|
|
34
|
+
id: "hero-1",
|
|
35
|
+
version: { logic: "1.0.0", media: "1.0.0", docs: "1.0.0" },
|
|
36
|
+
humanInfo: { name: "Hero", author: "test", url: "http://example.com/hero" },
|
|
37
|
+
downloadSources: ["http://example.com/hero.tar"],
|
|
38
|
+
pathVariables: {},
|
|
39
|
+
requiredPieces: { weapon: { expected: ["sword-1"], selectable: false } },
|
|
40
|
+
}],
|
|
41
|
+
weapon: [{
|
|
42
|
+
id: "sword-1",
|
|
43
|
+
version: { logic: "1.0.0", media: "1.0.0", docs: "1.0.0" },
|
|
44
|
+
humanInfo: { name: "Sword", author: "test", url: "http://example.com/sword" },
|
|
45
|
+
downloadSources: ["http://example.com/sword.tar"],
|
|
46
|
+
pathVariables: {},
|
|
47
|
+
requiredPieces: {},
|
|
48
|
+
}],
|
|
49
|
+
},
|
|
50
|
+
selection: {
|
|
51
|
+
piece: {
|
|
52
|
+
character: {
|
|
53
|
+
type: "normal",
|
|
54
|
+
pieceMeta: { schema: {}, defaultMeta: {}, pieceMeta: {} },
|
|
55
|
+
validation: { count: "*", unique: false, banList: [], customValidation: [] },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
globalValidation: [],
|
|
59
|
+
scriptDictionary: {},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function makeHeroSelection(): Record<string, Array<SelectedPiece>> {
|
|
65
|
+
return {
|
|
66
|
+
character: [{
|
|
67
|
+
id: "hero-1",
|
|
68
|
+
required: { weapon: { mandatory: [{ id: "sword-1", required: {} }], selectable: [] } },
|
|
69
|
+
}],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { gzipSync } from "node:zlib";
|
|
2
|
+
|
|
3
|
+
function octalField(value: number, length: number): string {
|
|
4
|
+
return value.toString(8).padStart(length - 1, "0") + "\0";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function tarHeader(name: string, size: number): Buffer {
|
|
8
|
+
const header = Buffer.alloc(512, 0);
|
|
9
|
+
header.write(name, 0, "utf8");
|
|
10
|
+
header.write(octalField(0o644, 8), 100, "ascii");
|
|
11
|
+
header.write(octalField(0, 8), 108, "ascii");
|
|
12
|
+
header.write(octalField(0, 8), 116, "ascii");
|
|
13
|
+
header.write(octalField(size, 12), 124, "ascii");
|
|
14
|
+
header.write(octalField(0, 12), 136, "ascii");
|
|
15
|
+
header.write(" ", 148, "ascii"); // chksum placeholder while summing
|
|
16
|
+
header.write("0", 156, "ascii"); // typeflag: regular file
|
|
17
|
+
header.write("ustar\0", 257, "ascii"); // magic
|
|
18
|
+
header.write("00", 263, "ascii"); // version
|
|
19
|
+
|
|
20
|
+
let checksum = 0;
|
|
21
|
+
for (const byte of header) checksum += byte;
|
|
22
|
+
header.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, "ascii");
|
|
23
|
+
|
|
24
|
+
return header;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Builds a minimal ustar+gzip buffer by hand, so the download test can
|
|
28
|
+
// exercise the real gzip/tar plugins without a `tar` build dependency.
|
|
29
|
+
// Mirrors core/plugin-runtime/test/helpers/make-tar-gz.ts.
|
|
30
|
+
export function makeTarGz(files: Array<{ path: string, content: string }>): Buffer {
|
|
31
|
+
const parts: Array<Buffer> = [];
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const content = Buffer.from(file.content, "utf8");
|
|
34
|
+
const paddedLength = Math.ceil(content.length / 512) * 512;
|
|
35
|
+
const padded = Buffer.alloc(paddedLength, 0);
|
|
36
|
+
content.copy(padded);
|
|
37
|
+
parts.push(tarHeader(file.path, content.length), padded);
|
|
38
|
+
}
|
|
39
|
+
parts.push(Buffer.alloc(1024, 0)); // end-of-archive marker
|
|
40
|
+
return gzipSync(Buffer.concat(parts));
|
|
41
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { join as pathJoin, dirname } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { RosterLockV1Config, RosterLockPiece } from "@roster-lock/types";
|
|
5
|
+
|
|
6
|
+
import { prepareDatabase } from "../../../src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema";
|
|
7
|
+
|
|
8
|
+
export const TEST_PIECE_TYPE = "character";
|
|
9
|
+
|
|
10
|
+
export function makeEngine(overrides: Partial<RosterLockV1Config["engine"]> = {}): RosterLockV1Config["engine"] {
|
|
11
|
+
return {
|
|
12
|
+
name: "test-engine",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
pieceDefinitions: {
|
|
15
|
+
[TEST_PIECE_TYPE]: {
|
|
16
|
+
selectionStrategy: "mandatory",
|
|
17
|
+
requires: [],
|
|
18
|
+
pathVariables: [],
|
|
19
|
+
assets: [
|
|
20
|
+
{ name: "sprite", classification: "media", count: 1, glob: ["sprite.png"] },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function makePiece(overrides: Partial<RosterLockPiece> = {}): RosterLockPiece {
|
|
29
|
+
return {
|
|
30
|
+
id: "test-piece",
|
|
31
|
+
version: { logic: "1.0.0", media: "1.0.0", docs: "1.0.0" },
|
|
32
|
+
humanInfo: { name: "Test Piece", author: "tester", url: "http://example.com" },
|
|
33
|
+
downloadSources: ["http://example.com/download"],
|
|
34
|
+
pathVariables: {},
|
|
35
|
+
requiredPieces: {},
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function makeTempFolder(): Promise<string> {
|
|
41
|
+
return mkdtemp(pathJoin(tmpdir(), "match-agent-test-"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Seeds the pieces table directly (bypassing the real download pipeline in
|
|
45
|
+
// ensurePieceExists/addNewPiece, which would require a real download plugin)
|
|
46
|
+
// and writes real files under the piece's folder on disk, since the routes
|
|
47
|
+
// under test read both the DB row and the filesystem.
|
|
48
|
+
export async function seedCompletePiece(opts: {
|
|
49
|
+
folder: string,
|
|
50
|
+
engine: RosterLockV1Config["engine"],
|
|
51
|
+
pieceType: string,
|
|
52
|
+
piece: RosterLockPiece,
|
|
53
|
+
folderName: string,
|
|
54
|
+
files: Record<string, string>,
|
|
55
|
+
complete?: boolean,
|
|
56
|
+
}) {
|
|
57
|
+
const { folder, engine, pieceType, piece, folderName, files, complete = true } = opts;
|
|
58
|
+
const pieceIndex = {
|
|
59
|
+
engineName: engine.name,
|
|
60
|
+
pieceType,
|
|
61
|
+
logic: piece.version.logic,
|
|
62
|
+
media: piece.version.media,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const db = prepareDatabase(pathJoin(folder, "rosterlock.sqlite3.db"));
|
|
66
|
+
try {
|
|
67
|
+
db.insertNewPiece(
|
|
68
|
+
{
|
|
69
|
+
engineName: engine.name,
|
|
70
|
+
pieceType,
|
|
71
|
+
version: piece.version,
|
|
72
|
+
humanInfo: piece.humanInfo,
|
|
73
|
+
pathVariables: piece.pathVariables,
|
|
74
|
+
downloadSources: piece.downloadSources,
|
|
75
|
+
},
|
|
76
|
+
folderName
|
|
77
|
+
);
|
|
78
|
+
if (complete) {
|
|
79
|
+
db.pieceSuccessfullyDownloaded(pieceIndex, piece.downloadSources[0] ?? "");
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
db.close();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pieceFolder = pathJoin(folder, engine.name, pieceType, folderName);
|
|
86
|
+
await mkdir(pieceFolder, { recursive: true });
|
|
87
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
88
|
+
const fullPath = pathJoin(pieceFolder, relativePath);
|
|
89
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
90
|
+
await writeFile(fullPath, contents);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return pieceFolder;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function cleanupFolder(folder: string) {
|
|
97
|
+
await rm(folder, { recursive: true, force: true });
|
|
98
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { installPlugin } from "@roster-lock/plugin-runtime";
|
|
6
|
+
|
|
7
|
+
const _require = createRequire(__filename);
|
|
8
|
+
|
|
9
|
+
// Installs real, already-built workspace dl-* plugins into a throwaway
|
|
10
|
+
// pluginDir via the actual `installPlugin` - mirrors
|
|
11
|
+
// core/plugin-runtime/test/helpers/plugin-dir.ts, duplicated here since that
|
|
12
|
+
// helper lives under plugin-runtime's test/ (not published) and match-agent
|
|
13
|
+
// needs the same fixture to exercise a real download through ensurePieceExists.
|
|
14
|
+
export async function createFixturePluginDir(packageNames: Array<string>) {
|
|
15
|
+
const pluginDir = await mkdtemp(join(tmpdir(), "match-agent-plugin-dir-"));
|
|
16
|
+
|
|
17
|
+
for (const packageName of packageNames) {
|
|
18
|
+
const pkgJsonPath = _require.resolve(`${packageName}/package.json`);
|
|
19
|
+
await installPlugin(pluginDir, dirname(pkgJsonPath));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
pluginDir,
|
|
24
|
+
cleanup: () => rm(pluginDir, { recursive: true, force: true }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MessageBridge } from "@roster-lock/utils";
|
|
2
|
+
|
|
3
|
+
// A minimal stand-in for the relay server's room orchestration (see the
|
|
4
|
+
// reference implementation in
|
|
5
|
+
// src/handle-room/version-1/room-handler-bridge/server.pseudo.ts). Drives
|
|
6
|
+
// the same request sequence against each user's "room side" bridge, so the
|
|
7
|
+
// real encryption/decryption + runSelection + handleDownloads logic inside
|
|
8
|
+
// bindStepsToBridge runs for real, without a relay server or network.
|
|
9
|
+
export async function driveRoomProtocol(users: Array<{ bridge: MessageBridge, publicKey: string }>) {
|
|
10
|
+
try {
|
|
11
|
+
const userCommits: Record<string, unknown> = {};
|
|
12
|
+
await Promise.all(users.map(async (user) => {
|
|
13
|
+
userCommits[user.publicKey] = await user.bridge.sendRequest("user-selection", {});
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const userReveals: Record<string, unknown> = {};
|
|
17
|
+
await Promise.all(users.map(async (user) => {
|
|
18
|
+
userReveals[user.publicKey] = await user.bridge.sendRequest("all-selection-for-user-decryption", userCommits);
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
await Promise.all(users.map(async (user) => {
|
|
22
|
+
await user.bridge.sendRequest("all-decryption-for-user-final", userReveals);
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
await Promise.all(users.map(async (user) => {
|
|
26
|
+
await user.bridge.sendRequest("user-download", {});
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
for (const user of users) {
|
|
30
|
+
user.bridge.sendEvent("all-download", {});
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Mirrors runRooms' error path in server.pseudo.ts: a failure on one
|
|
34
|
+
// user's leg has to be broadcast to every agent, since each agent's
|
|
35
|
+
// bindStepsToBridge promise otherwise just hangs waiting for a
|
|
36
|
+
// "all-download" event that will never come.
|
|
37
|
+
for (const user of users) {
|
|
38
|
+
user.bridge.sendEvent("error", (e as Error).message ?? String(e));
|
|
39
|
+
}
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AddressInfo } from "node:net";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
import { PluginManager } from "@roster-lock/plugin-runtime";
|
|
6
|
+
import { MatchAgentServer } from "../../../src/server";
|
|
7
|
+
import { authMiddleware, validateAuthCode } from "../../../src/authentication";
|
|
8
|
+
import { createV1Routers } from "../../../src/handle-room/version-1";
|
|
9
|
+
import { createEditorV1Router } from "../../../src/editor-support";
|
|
10
|
+
import { getSQLite3FolderDB } from "../../../src/handle-room/version-1/globals/FolderDB";
|
|
11
|
+
|
|
12
|
+
// Mirrors the wiring in src/index.ts#startServer, but keeps a handle to the
|
|
13
|
+
// underlying MatchAgentServer/http.Server so tests can bind an ephemeral
|
|
14
|
+
// port and shut the server down - startServer() itself returns void.
|
|
15
|
+
export async function startTestServer(folder: string, authCode: string = randomUUID(), pluginDir?: string) {
|
|
16
|
+
const server = new MatchAgentServer();
|
|
17
|
+
server.httpRouter.get("/", ({ res }) => {
|
|
18
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
19
|
+
res.end(JSON.stringify({ hello: "world" }));
|
|
20
|
+
});
|
|
21
|
+
server.httpRouter.post("/validate-authcode", validateAuthCode(authCode));
|
|
22
|
+
|
|
23
|
+
const pluginRuntime = await PluginManager.create(pluginDir ?? folder);
|
|
24
|
+
const fileDB = getSQLite3FolderDB(folder, pluginRuntime);
|
|
25
|
+
const { httpRouter: v1HttpRouter, wsRouter: v1WsRouter } = createV1Routers(fileDB, pluginRuntime);
|
|
26
|
+
server.httpRouter.use("/v1", authMiddleware(authCode), v1HttpRouter);
|
|
27
|
+
server.wsRouter.use("/v1", authMiddleware(authCode), v1WsRouter);
|
|
28
|
+
server.httpRouter.use("/editor/v1", authMiddleware(authCode), createEditorV1Router(pluginRuntime, fileDB));
|
|
29
|
+
|
|
30
|
+
const nodeServer = server.listen(0);
|
|
31
|
+
await once(nodeServer, "listening");
|
|
32
|
+
const { port } = nodeServer.address() as AddressInfo;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
authCode,
|
|
36
|
+
port,
|
|
37
|
+
httpUrl: `http://localhost:${port}`,
|
|
38
|
+
wsUrl: `ws://localhost:${port}`,
|
|
39
|
+
close(): Promise<void> {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
server.close((err?: Error) => err ? reject(err) : resolve());
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TestServer = Awaited<ReturnType<typeof startTestServer>>;
|
|
48
|
+
|
|
49
|
+
export async function errorBody(res: Response): Promise<{ error: string, context?: unknown }> {
|
|
50
|
+
return res.json() as Promise<{ error: string, context?: unknown }>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { RosterLockV1Config, SelectedPiece } from "@roster-lock/types";
|
|
3
|
+
|
|
4
|
+
function sha256(input: string) {
|
|
5
|
+
return createHash("sha256").update(input).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// makeLockConfig() (lockConfig.ts) is shaped for bindStepsToBridge tests,
|
|
9
|
+
// which never runs it through ROSTERLOCK_V1_CASTER_JSONSCHEMA (the AJV
|
|
10
|
+
// schema match-agent's HTTP routes validate lockConfig bodies against,
|
|
11
|
+
// e.g. piece-sort.ts) - that schema requires real sha256 hex hashes and
|
|
12
|
+
// https:// urls, which makeLockConfig()'s "1.0.0"/http:// fixtures fail.
|
|
13
|
+
// This is the same shape, just with values that pass that schema too.
|
|
14
|
+
export function makeValidLockConfig(): RosterLockV1Config {
|
|
15
|
+
return {
|
|
16
|
+
configIdentity: { namespace: "roster-lock", purpose: "lock", version: 1 },
|
|
17
|
+
author: "test",
|
|
18
|
+
title: "Test Config",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
engine: {
|
|
21
|
+
name: "test-engine",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
pieceDefinitions: {
|
|
24
|
+
character: {
|
|
25
|
+
selectionStrategy: "personal",
|
|
26
|
+
requires: ["weapon"],
|
|
27
|
+
pathVariables: [],
|
|
28
|
+
assets: [{ name: "model", classification: "media", count: 1, glob: ["model.glb"] }],
|
|
29
|
+
},
|
|
30
|
+
weapon: {
|
|
31
|
+
selectionStrategy: "on demand",
|
|
32
|
+
requires: [],
|
|
33
|
+
pathVariables: [],
|
|
34
|
+
assets: [{ name: "model", classification: "media", count: 1, glob: ["model.glb"] }],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
rosters: {
|
|
39
|
+
character: [{
|
|
40
|
+
id: "hero-1",
|
|
41
|
+
version: { logic: sha256("hero-1-logic"), media: sha256("hero-1-media"), docs: sha256("hero-1-docs") },
|
|
42
|
+
humanInfo: { name: "Hero", author: "test", url: "https://example.com/hero" },
|
|
43
|
+
downloadSources: ["https://example.com/hero.tar"],
|
|
44
|
+
pathVariables: {},
|
|
45
|
+
requiredPieces: { weapon: { expected: ["sword-1"], selectable: false } },
|
|
46
|
+
}],
|
|
47
|
+
weapon: [{
|
|
48
|
+
id: "sword-1",
|
|
49
|
+
version: { logic: sha256("sword-1-logic"), media: sha256("sword-1-media"), docs: sha256("sword-1-docs") },
|
|
50
|
+
humanInfo: { name: "Sword", author: "test", url: "https://example.com/sword" },
|
|
51
|
+
downloadSources: ["https://example.com/sword.tar"],
|
|
52
|
+
pathVariables: {},
|
|
53
|
+
requiredPieces: {},
|
|
54
|
+
}],
|
|
55
|
+
},
|
|
56
|
+
selection: {
|
|
57
|
+
piece: {
|
|
58
|
+
character: {
|
|
59
|
+
type: "normal",
|
|
60
|
+
validation: { count: "*", unique: false, banList: [], customValidation: [] },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
globalValidation: [],
|
|
64
|
+
scriptDictionary: {},
|
|
65
|
+
},
|
|
66
|
+
pieceMeta: {
|
|
67
|
+
character: { schema: {}, defaultMeta: {}, values: {} },
|
|
68
|
+
weapon: { schema: {}, defaultMeta: {}, values: {} },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function makeValidHeroSelection(): Record<string, Array<SelectedPiece>> {
|
|
74
|
+
return {
|
|
75
|
+
character: [{
|
|
76
|
+
id: "hero-1",
|
|
77
|
+
required: { weapon: { mandatory: [{ id: "sword-1", required: {} }], selectable: [] } },
|
|
78
|
+
}],
|
|
79
|
+
};
|
|
80
|
+
}
|