Skip to content

Commit 30d4331

Browse files
committed
Add project type contract tests for GameFoundryStudio - PR_26152_080-project-type-contract-tests
1 parent 101f318 commit 30d4331

6 files changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Project Type Contract Tests Validation
2+
3+
PR: `PR_26152_080-project-type-contract-tests`
4+
5+
## Scope
6+
7+
- Added Project Type contract rules to `src/shared/contracts/projectContract.js`.
8+
- Updated Project contract fixtures and targeted contract tests.
9+
- Updated Project Workspace contract test fixture so Projects include `projectType`.
10+
- Added `docs/dev/specs/PROJECT_TYPE_CONTRACT.md`.
11+
12+
No database, authentication, UI, CSS, HTML, or runtime page changes were made.
13+
14+
## Contract Rules Validated
15+
16+
- Every Project requires `projectType`.
17+
- Valid Project Types are accepted:
18+
- `game`
19+
- `asset-pack`
20+
- `music-pack`
21+
- `localization-pack`
22+
- `template`
23+
- `tutorial`
24+
- Invalid Project Types are rejected.
25+
- All Project Types still require owner.
26+
- All Project Types still require visibility.
27+
- Project Type determines expected outputs only.
28+
- Project Type does not bypass permissions.
29+
- Project Type does not create a different ownership model.
30+
- All Project Types share Project lifecycle and Project Workspace runtime-only model.
31+
32+
## Validation Commands
33+
34+
- `node tests/shared/ProjectContract.test.mjs` - PASS
35+
- `node tests/shared/ProjectWorkspaceRuntimeContract.test.mjs` - PASS
36+
- `node tests/shared/ToolStateContract.test.mjs` - PASS
37+
- `node tests/shared/IdentityPermissionsContract.test.mjs` - PASS
38+
- `$files = Get-ChildItem tests/shared/tools -Filter *.test.mjs | Sort-Object Name; foreach ($file in $files) { node $file.FullName; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } }` - PASS, 36 tool contract tests ran
39+
- `git diff --check -- src/shared/contracts/projectContract.js tests/shared/ProjectContract.test.mjs tests/shared/ProjectWorkspaceRuntimeContract.test.mjs tests/fixtures/projects/project-scenarios.json docs/dev/specs/PROJECT_TYPE_CONTRACT.md` - PASS
40+
- `git diff --name-only -- '*.css' '*.html' '*.js' ':!src/shared/contracts/projectContract.js' ':!src/shared/contracts/projectWorkspaceRuntimeContract.js' ':!src/shared/contracts/tools/*.js'` - PASS, no output
41+
42+
## Validation Lanes
43+
44+
- Lanes executed: contract validation for Project Type, Project Workspace compatibility, Project, Tool State, Identity/Permissions, and Tool contracts.
45+
- Lanes skipped: runtime, integration, engine, samples, and recovery/UAT because this PR does not change runtime behavior, handoff contracts, engine surfaces, samples, or recovery behavior.
46+
- Samples decision: SKIP because this PR is limited to contract/docs/test surfaces.
47+
- Playwright impacted: No. This PR is contract/docs/test only and does not change UI or browser runtime behavior.
48+
49+
## Expected PASS Behavior
50+
51+
- Project contract rejects missing or invalid `projectType`.
52+
- Project contract accepts all approved Project Types.
53+
- Owner, visibility, permission, lifecycle, and Project Workspace behavior remain shared across all Project Types.
54+
- Existing Project Workspace, Tool State, Identity/Permissions, and Tool contract tests continue to pass.
55+
56+
## Expected WARN Behavior
57+
58+
- Git may report line-ending normalization warnings for existing working-copy files. These warnings do not indicate whitespace failures and did not fail `git diff --check`.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Project Type Contract
2+
3+
## Status
4+
5+
This is a contract planning document for GameFoundryStudio Project Type behavior.
6+
7+
It defines project type rules for persisted Project records before database, authentication, UI, runtime, publishing, marketplace, or storage implementation begins.
8+
9+
This document does not authorize runtime changes, database implementation, authentication implementation, page changes, CSS changes, HTML changes, JavaScript changes, or schema migrations.
10+
11+
## Project Type Values
12+
13+
Approved first-pass Project Types:
14+
15+
- `game`
16+
- `asset-pack`
17+
- `music-pack`
18+
- `localization-pack`
19+
- `template`
20+
- `tutorial`
21+
22+
## Required Rules
23+
24+
- Every Project requires a `projectType`.
25+
- Project is always the persisted ownership container.
26+
- Project Type determines expected outputs, not ownership behavior.
27+
- All Project Types share the same ownership model.
28+
- All Project Types share the same visibility model.
29+
- All Project Types share the same permissions model.
30+
- All Project Types share the same lifecycle model.
31+
- All Project Types share the same Project Workspace runtime-only model.
32+
33+
## Expected Outputs
34+
35+
| Project Type | Expected Outputs |
36+
| --- | --- |
37+
| `game` | Game Manifest, Release |
38+
| `asset-pack` | Asset outputs |
39+
| `music-pack` | Audio outputs, MIDI outputs |
40+
| `localization-pack` | Translation outputs |
41+
| `template` | Reusable starter content |
42+
| `tutorial` | Learning or community content |
43+
44+
## Ownership Boundary
45+
46+
Project Type does not create a separate ownership container.
47+
48+
The Project remains the persisted ownership container for Project data, Tool States, project-owned assets, Game Manifest records, Releases, Marketplace Items, and other future child records unless a later object contract explicitly promotes separate ownership.
49+
50+
## Visibility And Permissions Boundary
51+
52+
Project Type does not bypass visibility or permissions.
53+
54+
Private Projects remain private regardless of type.
55+
56+
Collaborators, Viewers, Owners, Admins, Moderators, Reviewers, Players, and Guests continue to use the approved Identity/Permissions and Project contract rules.
57+
58+
## Project Workspace Boundary
59+
60+
Project Workspace remains runtime-only for every Project Type.
61+
62+
Project Workspace may track active project, active tool, active tool state, dirty status, recovery availability, active palette context, and open/close/save flow state.
63+
64+
Project Workspace does not persist tool payloads, own saved Tool State, duplicate Project storage, or create a separate Project Type storage model.
65+
66+
## Non-Goals
67+
68+
This document does not:
69+
70+
- define SQL schema
71+
- define API routes
72+
- implement project persistence
73+
- implement authentication
74+
- implement authorization checks
75+
- implement Project Workspace storage
76+
- implement Admin pages
77+
- implement Account pages
78+
- implement publishing
79+
- implement marketplace behavior
80+
- change runtime behavior
81+
- change CSS, HTML, JavaScript, TypeScript, or JSON files outside the contract/test scope

src/shared/contracts/projectContract.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@ import {
1616
export const PROJECT_CONTRACT_ID = "gamefoundrystudio.project.lifecycle";
1717
export const PROJECT_CONTRACT_VERSION = "1.0.0";
1818

19+
export const PROJECT_TYPES = Object.freeze({
20+
GAME: "game",
21+
ASSET_PACK: "asset-pack",
22+
MUSIC_PACK: "music-pack",
23+
LOCALIZATION_PACK: "localization-pack",
24+
TEMPLATE: "template",
25+
TUTORIAL: "tutorial",
26+
});
27+
28+
export const PROJECT_TYPE_LIST = Object.freeze([
29+
PROJECT_TYPES.GAME,
30+
PROJECT_TYPES.ASSET_PACK,
31+
PROJECT_TYPES.MUSIC_PACK,
32+
PROJECT_TYPES.LOCALIZATION_PACK,
33+
PROJECT_TYPES.TEMPLATE,
34+
PROJECT_TYPES.TUTORIAL,
35+
]);
36+
1937
export const PROJECT_STATES = Object.freeze({
2038
DRAFT: "draft",
2139
ACTIVE: "active",
@@ -78,6 +96,45 @@ export const PROJECT_RELATIONSHIP_LIST = Object.freeze([
7896
PROJECT_RELATIONSHIPS.MARKETPLACE_ITEMS,
7997
]);
8098

99+
export const PROJECT_TYPE_OUTPUTS = Object.freeze({
100+
ASSET_OUTPUTS: "asset-outputs",
101+
AUDIO_OUTPUTS: "audio-outputs",
102+
MIDI_OUTPUTS: "midi-outputs",
103+
TRANSLATION_OUTPUTS: "translation-outputs",
104+
REUSABLE_STARTER_CONTENT: "reusable-starter-content",
105+
LEARNING_COMMUNITY_CONTENT: "learning-community-content",
106+
});
107+
108+
export const PROJECT_TYPE_EXPECTED_OUTPUTS = Object.freeze({
109+
[PROJECT_TYPES.GAME]: Object.freeze([
110+
PROJECT_RELATIONSHIPS.GAME_MANIFEST,
111+
PROJECT_RELATIONSHIPS.RELEASES,
112+
]),
113+
[PROJECT_TYPES.ASSET_PACK]: Object.freeze([
114+
PROJECT_TYPE_OUTPUTS.ASSET_OUTPUTS,
115+
]),
116+
[PROJECT_TYPES.MUSIC_PACK]: Object.freeze([
117+
PROJECT_TYPE_OUTPUTS.AUDIO_OUTPUTS,
118+
PROJECT_TYPE_OUTPUTS.MIDI_OUTPUTS,
119+
]),
120+
[PROJECT_TYPES.LOCALIZATION_PACK]: Object.freeze([
121+
PROJECT_TYPE_OUTPUTS.TRANSLATION_OUTPUTS,
122+
]),
123+
[PROJECT_TYPES.TEMPLATE]: Object.freeze([
124+
PROJECT_TYPE_OUTPUTS.REUSABLE_STARTER_CONTENT,
125+
]),
126+
[PROJECT_TYPES.TUTORIAL]: Object.freeze([
127+
PROJECT_TYPE_OUTPUTS.LEARNING_COMMUNITY_CONTENT,
128+
]),
129+
});
130+
131+
export const PROJECT_TYPE_RULES = Object.freeze({
132+
PROJECT_IS_PERSISTED_OWNERSHIP_CONTAINER: true,
133+
PROJECT_TYPE_DETERMINES_EXPECTED_OUTPUTS_ONLY: true,
134+
SHARES_OWNERSHIP_VISIBILITY_PERMISSIONS_LIFECYCLE: true,
135+
SHARES_PROJECT_WORKSPACE_MODEL: true,
136+
});
137+
81138
export const PROJECT_ROLE_PERMISSION_GRANTS = Object.freeze({
82139
[PROJECT_ROLES.OWNER]: Object.freeze([
83140
IDENTITY_PERMISSIONS.VIEW,
@@ -103,6 +160,8 @@ export const PROJECT_INACTIVE_STATES = Object.freeze([
103160

104161
export const PROJECT_CONTRACT_ERRORS = Object.freeze({
105162
OWNER_REQUIRED: "PROJECT_OWNER_REQUIRED",
163+
PROJECT_TYPE_REQUIRED: "PROJECT_TYPE_REQUIRED",
164+
PROJECT_TYPE_INVALID: "PROJECT_TYPE_INVALID",
106165
VISIBILITY_REQUIRED: "PROJECT_VISIBILITY_REQUIRED",
107166
VISIBILITY_INVALID: "PROJECT_VISIBILITY_INVALID",
108167
STATE_REQUIRED: "PROJECT_STATE_REQUIRED",
@@ -115,6 +174,10 @@ export function isProjectState(value) {
115174
return PROJECT_STATE_LIST.includes(value);
116175
}
117176

177+
export function isProjectType(value) {
178+
return PROJECT_TYPE_LIST.includes(value);
179+
}
180+
118181
export function isProjectRole(value) {
119182
return PROJECT_ROLE_LIST.includes(value);
120183
}
@@ -138,6 +201,20 @@ export function validateProjectContract(project) {
138201
));
139202
}
140203

204+
if (!hasNonEmptyString(project?.projectType)) {
205+
errors.push(createContractError(
206+
PROJECT_CONTRACT_ERRORS.PROJECT_TYPE_REQUIRED,
207+
"Project records require projectType.",
208+
"projectType"
209+
));
210+
} else if (!isProjectType(project.projectType)) {
211+
errors.push(createContractError(
212+
PROJECT_CONTRACT_ERRORS.PROJECT_TYPE_INVALID,
213+
"Project type must be an allowed project type.",
214+
"projectType"
215+
));
216+
}
217+
141218
if (!hasNonEmptyString(project?.visibility)) {
142219
errors.push(createContractError(
143220
PROJECT_CONTRACT_ERRORS.VISIBILITY_REQUIRED,

tests/fixtures/projects/project-scenarios.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"project": {
66
"id": "project.private.draft",
77
"ownerId": "user.owner",
8+
"projectType": "game",
89
"state": "draft",
910
"visibility": "private",
1011
"members": [
@@ -26,6 +27,7 @@
2627
"project": {
2728
"id": "project.public.published",
2829
"ownerId": "user.owner",
30+
"projectType": "game",
2931
"state": "published",
3032
"visibility": "public",
3133
"members": [
@@ -43,6 +45,7 @@
4345
"project": {
4446
"id": "project.marketplace",
4547
"ownerId": "user.owner",
48+
"projectType": "asset-pack",
4649
"state": "marketplace",
4750
"visibility": "public",
4851
"members": [],
@@ -56,16 +59,39 @@
5659
"name": "missing owner",
5760
"project": {
5861
"id": "project.missing.owner",
62+
"projectType": "game",
5963
"state": "draft",
6064
"visibility": "private"
6165
},
6266
"expectedErrors": ["PROJECT_OWNER_REQUIRED"]
6367
},
68+
{
69+
"name": "missing project type",
70+
"project": {
71+
"id": "project.missing.type",
72+
"ownerId": "user.owner",
73+
"state": "draft",
74+
"visibility": "private"
75+
},
76+
"expectedErrors": ["PROJECT_TYPE_REQUIRED"]
77+
},
78+
{
79+
"name": "invalid project type",
80+
"project": {
81+
"id": "project.invalid.type",
82+
"ownerId": "user.owner",
83+
"projectType": "workspace",
84+
"state": "active",
85+
"visibility": "private"
86+
},
87+
"expectedErrors": ["PROJECT_TYPE_INVALID"]
88+
},
6489
{
6590
"name": "missing visibility",
6691
"project": {
6792
"id": "project.missing.visibility",
6893
"ownerId": "user.owner",
94+
"projectType": "game",
6995
"state": "active"
7096
},
7197
"expectedErrors": ["PROJECT_VISIBILITY_REQUIRED"]
@@ -75,6 +101,7 @@
75101
"project": {
76102
"id": "project.invalid.state",
77103
"ownerId": "user.owner",
104+
"projectType": "game",
78105
"state": "deleted",
79106
"visibility": "private"
80107
},
@@ -85,6 +112,7 @@
85112
"project": {
86113
"id": "project.invalid.role",
87114
"ownerId": "user.owner",
115+
"projectType": "game",
88116
"state": "active",
89117
"visibility": "project",
90118
"members": [
@@ -101,6 +129,7 @@
101129
"project": {
102130
"id": "project.invalid.relationship",
103131
"ownerId": "user.owner",
132+
"projectType": "game",
104133
"state": "active",
105134
"visibility": "project",
106135
"relationships": ["tool-states", "database-table"]

0 commit comments

Comments
 (0)