Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
432389220f | ||
|
|
e70159a81b |
@@ -55,7 +55,6 @@ Conflict-prevention rules:
|
|||||||
|
|
||||||
- `work/platform` owns `src/main/resources/db/migration/**`, Maven/dependency configuration, Compose, container build files, and CI workflow files. Other branches request schema changes instead of independently allocating migration versions.
|
- `work/platform` owns `src/main/resources/db/migration/**`, Maven/dependency configuration, Compose, container build files, and CI workflow files. Other branches request schema changes instead of independently allocating migration versions.
|
||||||
- `work/reports-ui` owns shared templates/fragments, shared design tokens, and general UI assets. Each domain branch owns its module-specific controllers and pages while consuming those shared fragments.
|
- `work/reports-ui` owns shared templates/fragments, shared design tokens, and general UI assets. Each domain branch owns its module-specific controllers and pages while consuming those shared fragments.
|
||||||
- A targeted repair shall use a clean, isolated `work/fix/<feature>/<what-fix>` branch and worktree from the taskmaster-verified current `main`. Do not use `work/<feature>/fix/<what-fix>`: the persistent `work/<feature>` ref already occupies that Git ref prefix.
|
|
||||||
- `work/tasks` exposes focused Task query/transfer operations required by Project workflows. `work/projects` owns the transaction that removes a member or completes a Project.
|
- `work/tasks` exposes focused Task query/transfer operations required by Project workflows. `work/projects` owns the transaction that removes a member or completes a Project.
|
||||||
- `work/platform` owns HolidayAPI credential storage and the tested HTTP client. `work/attendance` owns preview interpretation, selection, deduplication, import, and day-off effects.
|
- `work/platform` owns HolidayAPI credential storage and the tested HTTP client. `work/attendance` owns preview interpretation, selection, deduplication, import, and day-off effects.
|
||||||
- Attendance time and Task work time remain separate. No branch may make one mutate or prove the other.
|
- Attendance time and Task work time remain separate. No branch may make one mutate or prove the other.
|
||||||
|
|||||||
@@ -70,11 +70,6 @@ Use exactly these persistent branches unless the user changes the plan:
|
|||||||
| `work/attendance` | Policy, calendar, attendance, corrections, leave, metrics |
|
| `work/attendance` | Policy, calendar, attendance, corrections, leave, metrics |
|
||||||
| `work/reports-ui` | Shared Thymeleaf UI, dashboards, reports, exports |
|
| `work/reports-ui` | Shared Thymeleaf UI, dashboards, reports, exports |
|
||||||
|
|
||||||
For a targeted repair outside the next iteration, create a clean isolated
|
|
||||||
`work/fix/<feature>/<what-fix>` worktree from the taskmaster-verified current
|
|
||||||
`main`. Do not use `work/<feature>/fix/<what-fix>`: the persistent
|
|
||||||
`work/<feature>` ref already occupies that Git ref prefix.
|
|
||||||
|
|
||||||
Create one isolated worktree per branch. Give each implementation agent explicit ownership, tell it other agents share the repository, forbid reverting others' work, require medium-milestone local commits, and forbid push unless separately authorized.
|
Create one isolated worktree per branch. Give each implementation agent explicit ownership, tell it other agents share the repository, forbid reverting others' work, require medium-milestone local commits, and forbid push unless separately authorized.
|
||||||
|
|
||||||
Before any owner edits its module, require it to:
|
Before any owner edits its module, require it to:
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ For a multi-branch iteration:
|
|||||||
|
|
||||||
- Use one worktree and one named owner/subagent per branch. Tell every owner that other agents share the repository and it must not revert others' work.
|
- Use one worktree and one named owner/subagent per branch. Tell every owner that other agents share the repository and it must not revert others' work.
|
||||||
- Before starting assigned module work, every owner verifies its worktree is clean, fetches or uses the taskmaster-verified latest `main`, and fast-forwards its persistent branch to that exact main SHA. Do not build new work on a stale pre-integration branch, and do not use a merge that would rewrite or discard branch history.
|
- Before starting assigned module work, every owner verifies its worktree is clean, fetches or uses the taskmaster-verified latest `main`, and fast-forwards its persistent branch to that exact main SHA. Do not build new work on a stale pre-integration branch, and do not use a merge that would rewrite or discard branch history.
|
||||||
- A targeted repair uses a clean, isolated `work/fix/<feature>/<what-fix>` branch and worktree from the taskmaster-verified current `main`. Do not use `work/<feature>/fix/<what-fix>`: the persistent `work/<feature>` ref already occupies that Git ref prefix.
|
|
||||||
- Establish and commit the platform foundation before dependent persistence work.
|
- Establish and commit the platform foundation before dependent persistence work.
|
||||||
- Exchange only full immutable SHAs from clean worktrees; never merge a moving branch or ambiguous short SHA.
|
- Exchange only full immutable SHAs from clean worktrees; never merge a moving branch or ambiguous short SHA.
|
||||||
- Preserve branch ownership. Request a producer-owned service/DTO boundary instead of reading its tables from a consumer.
|
- Preserve branch ownership. Request a producer-owned service/DTO boundary instead of reading its tables from a consumer.
|
||||||
|
|||||||
@@ -62,14 +62,6 @@ npm ci
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use an isolated repair branch
|
|
||||||
|
|
||||||
For a targeted repair, start a clean worktree from the taskmaster-verified
|
|
||||||
current `main` on `work/fix/<feature>/<what-fix>`. Keep it separate from the
|
|
||||||
five persistent `work/<feature>` branches. Do not use
|
|
||||||
`work/<feature>/fix/<what-fix>` because the persistent `work/<feature>` ref
|
|
||||||
already occupies that Git ref prefix.
|
|
||||||
|
|
||||||
## 3. Start the development containers
|
## 3. Start the development containers
|
||||||
|
|
||||||
### PostgreSQL 18.4
|
### PostgreSQL 18.4
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ The product joins attendance oversight and Project delivery without pretending t
|
|||||||
3. **Attendance and Project work stay distinct.** The product may report them together, but one never derives or proves the other.
|
3. **Attendance and Project work stay distinct.** The product may report them together, but one never derives or proves the other.
|
||||||
4. **Deadlines are enforced at every path.** Scheduled workers improve timeliness, while request-time guards preserve correctness when scheduling is late.
|
4. **Deadlines are enforced at every path.** Scheduled workers improve timeliness, while request-time guards preserve correctness when scheduling is late.
|
||||||
5. **Prefer explicit, reviewable operations.** Feature-owned controller/service/repository flows, constrained state transitions, focused integrations, and shared report datasets serve clarity over speculative machinery.
|
5. **Prefer explicit, reviewable operations.** Feature-owned controller/service/repository flows, constrained state transitions, focused integrations, and shared report datasets serve clarity over speculative machinery.
|
||||||
6. **Fixes preserve branch ownership.** A targeted repair uses a clean `work/fix/<feature>/<what-fix>` branch from verified `main`, not `work/<feature>/fix/<what-fix>`; persistent `work/<feature>` refs already occupy that Git ref prefix.
|
|
||||||
|
|
||||||
## Accessibility & Inclusion
|
## Accessibility & Inclusion
|
||||||
|
|
||||||
|
|||||||
@@ -126,12 +126,6 @@ companion record under [`docs/tests`](docs/tests/README.md).
|
|||||||
| `work/attendance` | Policy, calendar, attendance workflows |
|
| `work/attendance` | Policy, calendar, attendance workflows |
|
||||||
| `work/reports-ui` | Shared UI, dashboards, reporting presentation |
|
| `work/reports-ui` | Shared UI, dashboards, reporting presentation |
|
||||||
|
|
||||||
For a targeted repair, create a clean isolated branch and worktree from the
|
|
||||||
taskmaster-verified current `main` named
|
|
||||||
`work/fix/<feature>/<what-fix>`. Do not nest it as
|
|
||||||
`work/<feature>/fix/<what-fix>`: the persistent `work/<feature>` ref already
|
|
||||||
uses that Git ref prefix.
|
|
||||||
|
|
||||||
Iteration 2 work must start from the merged Iteration 1 `main`, continue with
|
Iteration 2 work must start from the merged Iteration 1 `main`, continue with
|
||||||
strict RED-to-GREEN TDD, add Javadoc during implementation, and update the
|
strict RED-to-GREEN TDD, add Javadoc during implementation, and update the
|
||||||
matching Markdown evidence record before each milestone commit.
|
matching Markdown evidence record before each milestone commit.
|
||||||
|
|||||||
@@ -120,12 +120,6 @@ Simple configuration or documentation changes use the smallest useful shell
|
|||||||
check, followed by the affected Maven suite. Do not create an artificial Java
|
check, followed by the affected Maven suite. Do not create an artificial Java
|
||||||
test only to check that a text file exists.
|
test only to check that a text file exists.
|
||||||
|
|
||||||
Run that check from the clean targeted-fix branch named
|
|
||||||
`work/fix/<feature>/<what-fix>` when repairing one feature. Do not use
|
|
||||||
`work/<feature>/fix/<what-fix>`: a persistent `work/<feature>` ref already
|
|
||||||
occupies that Git ref prefix. Record the expected RED and the matching GREEN
|
|
||||||
shell output in the evidence record.
|
|
||||||
|
|
||||||
## 4. Useful commands
|
## 4. Useful commands
|
||||||
|
|
||||||
Run one test method:
|
Run one test method:
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# Access, Navigation, Icon, and Intern Picker Fix Plan
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
Record the durable workflow for the targeted fixes in this plan. This plan does
|
|
||||||
not change product behavior, dependencies, schemas, or the five persistent
|
|
||||||
feature-branch ownership areas.
|
|
||||||
|
|
||||||
## Implementation steps
|
|
||||||
|
|
||||||
1. Prove RED: the contributor guides lack the realizable repair-branch name.
|
|
||||||
2. Add one branch rule to contributor guides, design records, plans, and tracked
|
|
||||||
coordination authority: `work/fix/<feature>/<what-fix>` from verified
|
|
||||||
`main`.
|
|
||||||
3. State why `work/<feature>/fix/<what-fix>` is invalid while its persistent
|
|
||||||
`work/<feature>` ref exists.
|
|
||||||
4. Regenerate the local SRS after amending the existing operational requirement;
|
|
||||||
do not add a requirement ID or a use case.
|
|
||||||
5. Prove GREEN with the executable six-guide regression that independently
|
|
||||||
rejects a positive nested-form recommendation in every guide,
|
|
||||||
coordination-authority consistency, requirement/use-case counts, local-link
|
|
||||||
resolution, and an immutable base-to-candidate whitespace check. Commit the
|
|
||||||
tracked guidance locally; do not push or merge.
|
|
||||||
|
|
||||||
## Exit criteria
|
|
||||||
|
|
||||||
- The tracked guides, design record, implementation plan, root coordination
|
|
||||||
authority, and evidence record agree on the same repair-branch spelling.
|
|
||||||
- The local authoritative, explained, simple, and generated SRS catalogues keep
|
|
||||||
exactly 260 unique requirement IDs and the SRS keeps 14 use cases.
|
|
||||||
- The forbidden nested form is documented only as forbidden, not as a usable
|
|
||||||
branch name.
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Design Record: Durable Fix-Branch Workflow
|
|
||||||
|
|
||||||
- **Date:** 2026-08-15
|
|
||||||
- **Status:** Accepted
|
|
||||||
- **Related plan:** [Access, Navigation, Icon, and Intern Picker Fix Plan](../plans/2026-08-15-access-navigation-icon-intern-picker.md)
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The repository keeps five persistent feature refs: `work/platform`,
|
|
||||||
`work/projects`, `work/tasks`, `work/attendance`, and `work/reports-ui`.
|
|
||||||
A proposed nested repair name such as `work/platform/fix/example` cannot coexist
|
|
||||||
with the existing `work/platform` ref because Git cannot use one ref as both a
|
|
||||||
leaf and a prefix.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
Use `work/fix/<feature>/<what-fix>` for each targeted repair. Create its clean,
|
|
||||||
isolated worktree from the taskmaster-verified current `main`. The `<feature>`
|
|
||||||
segment identifies the owning persistent area; it does not nest below that
|
|
||||||
persistent branch.
|
|
||||||
|
|
||||||
The forbidden form is `work/<feature>/fix/<what-fix>`. A repair owner preserves
|
|
||||||
other worktrees, records RED and GREEN evidence, commits locally, and does not
|
|
||||||
push or merge without separate authority.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- Persistent feature branches remain available for their iteration ownership.
|
|
||||||
- A repair can be reviewed and handed off as one immutable branch head.
|
|
||||||
- Contributor documentation, local coordination authority, and generated SRS
|
|
||||||
traceability use the same spelling.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
The executable documentation validator checks the exact approved statement in
|
|
||||||
each of the six tracked guides and independently rejects an injected positive
|
|
||||||
nested-branch recommendation in every guide. The copied root coordination
|
|
||||||
authority uses the same rule and is checked separately for consistency. The
|
|
||||||
evidence record also verifies requirement counts, generated SRS use-case count,
|
|
||||||
and local Markdown links.
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# Test Evidence: Eligible Intern picker query
|
||||||
|
|
||||||
|
- **Test type:** Integration
|
||||||
|
- **Requirement IDs:** ACC-014, ACC-019–ACC-021, AUTH-001, PRJ-017, TST-001–TST-010
|
||||||
|
- **Scenario IDs:** AC-ACC-009, AC-ACC-010, AC-PRJ-010 (selection-eligibility support)
|
||||||
|
- **Test class/method:** com.lab.labtimesheet.feature.account.service.EligibleInternOptionIntegrationTest#listsOnlyActiveInternsWithActiveInclusiveInternshipsInPickerOrder; #rejectsMissingBusinessDate
|
||||||
|
- **Implementation commit:** e70159a81b6445825f6d5f912ecf3c4aa3c1aa85
|
||||||
|
|
||||||
|
## Protected behavior
|
||||||
|
|
||||||
|
Pending, locked, deactivated, non-Intern, not-started, completed, and date-expired records must not appear in the
|
||||||
|
Account-owned Intern picker. An option is selectable only when both account and internship are ACTIVE and the
|
||||||
|
explicit business date lies within the inclusive internship range. The returned numeric user ID is the internal
|
||||||
|
submission identity, and options sort by display name then student code.
|
||||||
|
The public query rejects a missing business date with the documented actionable message instead of issuing an
|
||||||
|
ambiguous null-bound database query.
|
||||||
|
|
||||||
|
## Test method
|
||||||
|
|
||||||
|
The PostgreSQL 18.4 integration test persists valid account/profile combinations through the account feature's JPA
|
||||||
|
entities and repositories. It uses SQL only as a test fixture for future lock, deactivation, and completion states
|
||||||
|
whose production transitions are outside this change. It calls the public Account service query and compares the
|
||||||
|
complete immutable DTO sequence, including both inclusive date boundaries and unique user IDs.
|
||||||
|
Its separate null-date regression calls the same public service method and asserts the exact
|
||||||
|
<code>IllegalArgumentException</code> message documented by that method.
|
||||||
|
|
||||||
|
## Hand-derived expected result
|
||||||
|
|
||||||
|
For business date 2026-08-14, profiles starting on that date and ending on that date remain eligible. The only
|
||||||
|
expected options are Alpha / STU-100, Alpha / STU-200, and Zeta / STU-300, in that order. Every other seeded
|
||||||
|
row fails at least one account role/state, internship state, or inclusive date condition.
|
||||||
|
For a missing business date, the service must immediately throw
|
||||||
|
<code>IllegalArgumentException("Business date is required")</code>.
|
||||||
|
|
||||||
|
## RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw -Dtest=EligibleInternOptionIntegrationTest test
|
||||||
|
~~~
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
[ERROR] EligibleInternOptionIntegrationTest.java:[11,54] cannot find symbol
|
||||||
|
symbol: class EligibleInternOption
|
||||||
|
location: package com.lab.labtimesheet.feature.account.model.dto
|
||||||
|
BUILD FAILURE
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw -Dtest=EligibleInternOptionIntegrationTest test
|
||||||
|
~~~
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
PostgreSQL 18.4 Testcontainers started and Flyway applied V1 baseline.
|
||||||
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## Review follow-up: missing business date
|
||||||
|
|
||||||
|
The public guard was temporarily removed solely to prove the new regression fails for the intended reason, then
|
||||||
|
restored exactly before the GREEN checks. The follow-up commit contains only the regression test and evidence.
|
||||||
|
|
||||||
|
### RED
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw '-Dtest=EligibleInternOptionIntegrationTest#rejectsMissingBusinessDate' test
|
||||||
|
~~~
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
|
||||||
|
java.lang.AssertionError: Expecting code to raise a throwable.
|
||||||
|
BUILD FAILURE
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### GREEN
|
||||||
|
|
||||||
|
**Command**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw '-Dtest=EligibleInternOptionIntegrationTest#rejectsMissingBusinessDate' test
|
||||||
|
~~~
|
||||||
|
|
||||||
|
**Observed result**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
PostgreSQL 18.4 Testcontainers started and Flyway applied V1 baseline.
|
||||||
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## Affected suite
|
||||||
|
|
||||||
|
**Command and result**
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw -Dtest=EligibleInternOptionIntegrationTest,AccountActivationIntegrationTest,BootstrapIntegrationTest,AccountWebIntegrationTest,AuthenticationWebIntegrationTest,BootstrapOnboardingWebIntegrationTest test
|
||||||
|
|
||||||
|
Selected account reports: 13 tests, 0 failures, 0 errors, 0 skipped.
|
||||||
|
|
||||||
|
./mvnw -Dtest=AccountActivationIntegrationTest test
|
||||||
|
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
|
||||||
|
./mvnw -Dtest=LayerStructureTest test
|
||||||
|
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
|
||||||
|
./mvnw test
|
||||||
|
Tests run: 105, Failures: 0, Errors: 0, Skipped: 0
|
||||||
|
BUILD SUCCESS
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Review follow-up affected account-service checks
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@25
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock
|
||||||
|
./mvnw '-Dtest=EligibleInternOptionIntegrationTest,AccountActivationIntegrationTest,BootstrapIntegrationTest' test
|
||||||
|
|
||||||
|
EligibleInternOptionIntegrationTest: 2 tests, 0 failures, 0 errors, 0 skipped
|
||||||
|
BootstrapIntegrationTest: 4 tests, 0 failures, 0 errors, 0 skipped
|
||||||
|
AccountActivationIntegrationTest: 2 tests, 0 failures, 0 errors, 0 skipped
|
||||||
|
Selected account-service reports: 8 tests, 0 failures, 0 errors, 0 skipped.
|
||||||
|
BUILD SUCCESS
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## External-test boundaries
|
||||||
|
|
||||||
|
This query does not authorize Project membership itself; the consuming Project transaction must still recheck
|
||||||
|
membership and ownership invariants. It does not test the later lifecycle mutation workflows that produce locked,
|
||||||
|
deactivated, or completed rows.
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
# Test Evidence: durable fix-branch documentation workflow
|
|
||||||
|
|
||||||
- **Test type:** Unit (documentation contract)
|
|
||||||
- **Requirement IDs:** `OPS-019`, `TST-009`, `TST-010`
|
|
||||||
- **Scenario IDs:** `AC-TST-001`
|
|
||||||
- **Test class/method:** `scripts/verify-fix-branch-workflow.cjs --self-test`
|
|
||||||
- **Implementation commit:** `f013ad7707b36959ddca891fe0d52f81bba3ee80`
|
|
||||||
- **Review-fix commit:** `d617769499362e92d058684501af3c1ae6b145b0`
|
|
||||||
|
|
||||||
## Protected behavior
|
|
||||||
|
|
||||||
Targeted repairs use the realizable `work/fix/<feature>/<what-fix>` branch and
|
|
||||||
clean worktree from taskmaster-verified `main`. Contributor guidance must reject
|
|
||||||
the impossible `work/<feature>/fix/<what-fix>` form while persistent
|
|
||||||
`work/<feature>` refs exist.
|
|
||||||
|
|
||||||
## Test method
|
|
||||||
|
|
||||||
Use the tracked Node validator rather than an artificial Java test. It requires
|
|
||||||
the exact approved statement in each of the six guides, then self-tests that an
|
|
||||||
extra positive nested-branch recommendation is rejected. The original RED
|
|
||||||
proves the required branch name was absent from the four contributor guides;
|
|
||||||
the review-fix RED proves the executable regression was absent.
|
|
||||||
|
|
||||||
## Hand-derived expected result
|
|
||||||
|
|
||||||
The required fix-branch spelling appears exactly once in each of the six tracked
|
|
||||||
documentation artifacts, and the only nested-form reference is inside that
|
|
||||||
artifact's exact approved statement. A simulated positive nested-branch
|
|
||||||
recommendation must fail. The existing SRS generator must still report 260
|
|
||||||
requirements and 14 use cases.
|
|
||||||
|
|
||||||
## RED
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
```text
|
|
||||||
rg -n -F 'work/fix/<feature>/<what-fix>' AGENTS.md README.md DEVELOPMENT.md TESTING.md
|
|
||||||
```
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
```text
|
|
||||||
exit 1; no matching lines
|
|
||||||
```
|
|
||||||
|
|
||||||
The failure was expected: the required realizable repair-branch rule was absent
|
|
||||||
before this documentation change.
|
|
||||||
|
|
||||||
### Review-fix RED
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
```text
|
|
||||||
node scripts/verify-fix-branch-workflow.cjs --self-test
|
|
||||||
```
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
```text
|
|
||||||
exit 1
|
|
||||||
Error: Cannot find module '.../scripts/verify-fix-branch-workflow.cjs'
|
|
||||||
```
|
|
||||||
|
|
||||||
The executable regression required to reject a positive nested-branch
|
|
||||||
recommendation did not exist.
|
|
||||||
|
|
||||||
## GREEN
|
|
||||||
|
|
||||||
**Command**
|
|
||||||
|
|
||||||
```text
|
|
||||||
node scripts/verify-fix-branch-workflow.cjs --self-test
|
|
||||||
```
|
|
||||||
|
|
||||||
**Observed result**
|
|
||||||
|
|
||||||
```text
|
|
||||||
Fix-branch workflow documentation: 6 approved statements validated
|
|
||||||
Positive nested branch recommendation: rejected
|
|
||||||
```
|
|
||||||
|
|
||||||
## Affected suite
|
|
||||||
|
|
||||||
**Command and result**
|
|
||||||
|
|
||||||
```text
|
|
||||||
node labtimesheet-docs-hub/ui-mockups/build-srs.cjs
|
|
||||||
node -e 'const fs=require("node:fs"); const checks=[["authoritative","labtimesheet-docs-hub/requirements-specification.md",/^\| ([A-Z]{2,4}-\d{3}) \|/gm],["explained","labtimesheet-docs-hub/explained/requirements-specification.md",/^\| ([A-Z]{2,4}-\d{3}) \|/gm],["simple","labtimesheet-docs-hub/explained/requirements-specification-simple.md",/^- \*\*([A-Z]{2,4}-\d{3}):\*\*/gm],["generated SRS","labtimesheet-docs-hub/software-requirements-specification.md",/^\| ([A-Z]{2,4}-\d{3}) \|/gm]]; for (const [name,file,pattern] of checks) { const ids=[...fs.readFileSync(file,"utf8").matchAll(pattern)].map(match=>match[1]); if (ids.length !== 260 || new Set(ids).size !== 260) throw new Error(`${name}: ${ids.length} rows, ${new Set(ids).size} unique`); console.log(`${name}: ${ids.length} rows, ${new Set(ids).size} unique IDs`); } const srs=fs.readFileSync("labtimesheet-docs-hub/software-requirements-specification.md","utf8"); const useCases=(srs.match(/^### 5\.\d+ UC-\d{2} —/gm)||[]).length; if (useCases !== 14) throw new Error(`SRS use cases: ${useCases}`); console.log(`generated SRS: ${useCases} use cases`);'
|
|
||||||
node -e 'const fs=require("node:fs"); const path=require("node:path"); let checked=0; const broken=[]; for (const file of process.argv.slice(1)) { const text=fs.readFileSync(file,"utf8"); for (const match of text.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) { const target=match[1].trim().replace(/^<|>$/g,"").split("#")[0].split("?")[0]; if (!target || /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) continue; checked += 1; if (!fs.existsSync(path.resolve(path.dirname(file), decodeURIComponent(target)))) broken.push(`${file}: ${target}`); } } if (broken.length) throw new Error(`Broken local Markdown links:\n${broken.join("\n")}`); console.log(`Local Markdown links: ${checked} resolved`);' AGENTS.md README.md DEVELOPMENT.md TESTING.md docs/superpowers/specs/2026-08-15-access-navigation-icon-intern-picker-design.md docs/superpowers/plans/2026-08-15-access-navigation-icon-intern-picker.md docs/tests/unit/fix-branch-workflow-documentation.md
|
|
||||||
git diff --check 8be1b754e188367b260981718a5d33fc2d4d8a3b d617769499362e92d058684501af3c1ae6b145b0
|
|
||||||
```
|
|
||||||
|
|
||||||
The SRS regeneration and count assertion ran from the main root because the
|
|
||||||
ignored requirements hub is local authority there. The link assertion and
|
|
||||||
the exact base-to-candidate `git diff --check` ran from this fix worktree; the
|
|
||||||
SRS generator also rejects a broken local SRS target before it writes the
|
|
||||||
generated file.
|
|
||||||
|
|
||||||
```text
|
|
||||||
Wrote labtimesheet-docs-hub/software-requirements-specification.md
|
|
||||||
Requirements: 260; use cases: 14; screens: 48; mockup embeds: 48
|
|
||||||
authoritative: 260 rows, 260 unique IDs
|
|
||||||
explained: 260 rows, 260 unique IDs
|
|
||||||
simple: 260 rows, 260 unique IDs
|
|
||||||
generated SRS: 260 rows, 260 unique IDs
|
|
||||||
generated SRS: 14 use cases
|
|
||||||
Fix-branch workflow documentation: 6 approved statements validated
|
|
||||||
Positive nested branch recommendation: rejected
|
|
||||||
Local Markdown links: 7 resolved
|
|
||||||
git diff --check 8be1b754e188367b260981718a5d33fc2d4d8a3b d617769499362e92d058684501af3c1ae6b145b0: exit 0
|
|
||||||
```
|
|
||||||
|
|
||||||
## External-test boundaries
|
|
||||||
|
|
||||||
This documentation contract does not create or manipulate Git branches, start
|
|
||||||
the application, or replace branch-owner review. It validates the durable rule
|
|
||||||
and SRS traceability only; a taskmaster still authorizes branch creation,
|
|
||||||
integration, and any push.
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
const assert = require('node:assert/strict');
|
|
||||||
const fs = require('node:fs');
|
|
||||||
const path = require('node:path');
|
|
||||||
|
|
||||||
const repositoryRoot = path.resolve(__dirname, '..');
|
|
||||||
const validForm = '`work/fix/<feature>/<what-fix>`';
|
|
||||||
const invalidForm = '`work/<feature>/fix/<what-fix>`';
|
|
||||||
const documents = [
|
|
||||||
{
|
|
||||||
file: 'AGENTS.md',
|
|
||||||
approved: '- A targeted repair uses a clean, isolated `work/fix/<feature>/<what-fix>` branch and worktree from the taskmaster-verified current `main`. Do not use `work/<feature>/fix/<what-fix>`: the persistent `work/<feature>` ref already occupies that Git ref prefix.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'README.md',
|
|
||||||
approved: 'For a targeted repair, create a clean isolated branch and worktree from the\ntaskmaster-verified current `main` named\n`work/fix/<feature>/<what-fix>`. Do not nest it as\n`work/<feature>/fix/<what-fix>`: the persistent `work/<feature>` ref already\nuses that Git ref prefix.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'DEVELOPMENT.md',
|
|
||||||
approved: 'For a targeted repair, start a clean worktree from the taskmaster-verified\ncurrent `main` on `work/fix/<feature>/<what-fix>`. Keep it separate from the\nfive persistent `work/<feature>` branches. Do not use\n`work/<feature>/fix/<what-fix>` because the persistent `work/<feature>` ref\nalready occupies that Git ref prefix.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'TESTING.md',
|
|
||||||
approved: 'Run that check from the clean targeted-fix branch named\n`work/fix/<feature>/<what-fix>` when repairing one feature. Do not use\n`work/<feature>/fix/<what-fix>`: a persistent `work/<feature>` ref already\noccupies that Git ref prefix. Record the expected RED and the matching GREEN\nshell output in the evidence record.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'docs/superpowers/specs/2026-08-15-access-navigation-icon-intern-picker-design.md',
|
|
||||||
approved: 'Use `work/fix/<feature>/<what-fix>` for each targeted repair. Create its clean,\nisolated worktree from the taskmaster-verified current `main`. The `<feature>`\nsegment identifies the owning persistent area; it does not nest below that\npersistent branch.\n\nThe forbidden form is `work/<feature>/fix/<what-fix>`. A repair owner preserves\nother worktrees, records RED and GREEN evidence, commits locally, and does not\npush or merge without separate authority.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'docs/superpowers/plans/2026-08-15-access-navigation-icon-intern-picker.md',
|
|
||||||
approved: '2. Add one branch rule to contributor guides, design records, plans, and tracked\n coordination authority: `work/fix/<feature>/<what-fix>` from verified\n `main`.\n3. State why `work/<feature>/fix/<what-fix>` is invalid while its persistent\n `work/<feature>` ref exists.'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Validates that every tracked guide contains only its approved branch wording. */
|
|
||||||
function validate(contents) {
|
|
||||||
const failures = [];
|
|
||||||
|
|
||||||
for (const {file, approved} of documents) {
|
|
||||||
const content = contents.get(file);
|
|
||||||
if (count(content, validForm) !== 1) failures.push(`${file} must contain ${validForm} exactly once`);
|
|
||||||
if (!content.includes(approved)) failures.push(`${file} is missing its approved branch workflow statement`);
|
|
||||||
|
|
||||||
const outsideApprovedStatement = content.replace(approved, '');
|
|
||||||
if (outsideApprovedStatement.includes(validForm) || outsideApprovedStatement.includes(invalidForm)) {
|
|
||||||
failures.push(`${file} contains an unapproved branch-form reference`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failures.length) throw new Error(failures.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function count(content, value) {
|
|
||||||
return content.split(value).length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readContents() {
|
|
||||||
return new Map(documents.map(({file}) => [file, fs.readFileSync(path.join(repositoryRoot, file), 'utf8')]));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contents = readContents();
|
|
||||||
validate(contents);
|
|
||||||
|
|
||||||
if (process.argv.includes('--self-test')) {
|
|
||||||
for (const {file} of documents) {
|
|
||||||
const positiveRecommendation = new Map(contents);
|
|
||||||
positiveRecommendation.set(file, `${contents.get(file)}\nUse ${invalidForm} for a targeted repair.\n`);
|
|
||||||
assert.throws(
|
|
||||||
() => validate(positiveRecommendation),
|
|
||||||
(error) => error instanceof Error
|
|
||||||
&& error.message.includes(`${file} contains an unapproved branch-form reference`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Fix-branch workflow documentation: ${documents.length} approved statements validated`);
|
|
||||||
if (process.argv.includes('--self-test')) {
|
|
||||||
console.log(`Positive nested branch recommendations: ${documents.length}/${documents.length} rejected`);
|
|
||||||
}
|
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package com.lab.labtimesheet.feature.account.model.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable non-secret selection data for an eligible Intern.
|
||||||
|
* The numeric user ID is the internal form submission identity; displayed fields are not authorization identifiers.
|
||||||
|
*
|
||||||
|
* @param userId persistent account identifier submitted by a consuming form
|
||||||
|
* @param displayName user-facing Intern name
|
||||||
|
* @param studentCode university student code shown to distinguish Interns
|
||||||
|
* @param internshipStart inclusive internship eligibility start date
|
||||||
|
* @param internshipEnd inclusive internship eligibility end date
|
||||||
|
*/
|
||||||
|
public record EligibleInternOption(
|
||||||
|
long userId,
|
||||||
|
String displayName,
|
||||||
|
String studentCode,
|
||||||
|
LocalDate internshipStart,
|
||||||
|
LocalDate internshipEnd) {
|
||||||
|
}
|
||||||
+32
@@ -1,8 +1,12 @@
|
|||||||
package com.lab.labtimesheet.feature.account.repository;
|
package com.lab.labtimesheet.feature.account.repository;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
||||||
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
import com.lab.labtimesheet.feature.account.model.InternshipStatus;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
||||||
import jakarta.persistence.LockModeType;
|
import jakarta.persistence.LockModeType;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -19,6 +23,34 @@ public interface InternProfileRepository extends JpaRepository<InternProfile, Lo
|
|||||||
boolean existsByUserIdAndInternshipStatusAndInternshipStartDateLessThanEqualAndInternshipEndDateGreaterThanEqual(
|
boolean existsByUserIdAndInternshipStatusAndInternshipStartDateLessThanEqualAndInternshipEndDateGreaterThanEqual(
|
||||||
Long userId, InternshipStatus status, LocalDate latestStartDate, LocalDate earliestEndDate);
|
Long userId, InternshipStatus status, LocalDate latestStartDate, LocalDate earliestEndDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projects account-owned non-secret selection data for Interns eligible on one inclusive business date.
|
||||||
|
* Results are ordered by display name, student code, then user ID for deterministic form rendering.
|
||||||
|
*
|
||||||
|
* @param globalRole required immutable Intern role
|
||||||
|
* @param accountStatus required active account state
|
||||||
|
* @param internshipStatus required active internship state
|
||||||
|
* @param businessDate date that must fall within the inclusive internship range
|
||||||
|
* @return eligible Intern selection projections without duplicate profile rows
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
select new com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption(
|
||||||
|
u.id, u.displayName, p.studentCode, p.internshipStartDate, p.internshipEndDate)
|
||||||
|
from InternProfile p
|
||||||
|
join AppUser u on u.id = p.userId
|
||||||
|
where u.globalRole = :globalRole
|
||||||
|
and u.accountStatus = :accountStatus
|
||||||
|
and p.internshipStatus = :internshipStatus
|
||||||
|
and p.internshipStartDate <= :businessDate
|
||||||
|
and p.internshipEndDate >= :businessDate
|
||||||
|
order by u.displayName asc, p.studentCode asc, u.id asc
|
||||||
|
""")
|
||||||
|
List<EligibleInternOption> findEligibleInternOptions(
|
||||||
|
@Param("globalRole") GlobalRole globalRole,
|
||||||
|
@Param("accountStatus") AccountStatus accountStatus,
|
||||||
|
@Param("internshipStatus") InternshipStatus internshipStatus,
|
||||||
|
@Param("businessDate") LocalDate businessDate);
|
||||||
|
|
||||||
/** Counts Intern profiles in a lifecycle state. */
|
/** Counts Intern profiles in a lifecycle state. */
|
||||||
long countByInternshipStatus(InternshipStatus status);
|
long countByInternshipStatus(InternshipStatus status);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.time.Clock;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
import com.lab.labtimesheet.feature.account.model.AccountStatus;
|
||||||
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
||||||
@@ -17,6 +18,7 @@ import com.lab.labtimesheet.feature.account.model.dto.AccountCreation;
|
|||||||
import com.lab.labtimesheet.feature.account.model.dto.AccountIdentity;
|
import com.lab.labtimesheet.feature.account.model.dto.AccountIdentity;
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.AccountSummary;
|
import com.lab.labtimesheet.feature.account.model.dto.AccountSummary;
|
||||||
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountCommand;
|
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountCommand;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
||||||
import com.lab.labtimesheet.feature.account.model.entity.UserActionToken;
|
import com.lab.labtimesheet.feature.account.model.entity.UserActionToken;
|
||||||
@@ -238,6 +240,24 @@ public class AccountService {
|
|||||||
.isPresent();
|
.isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists non-secret Intern selection options eligible on an explicit business date.
|
||||||
|
* The result requires active account and internship states plus inclusive internship dates, but it does not
|
||||||
|
* authorize a consuming Project operation; that operation must recheck its own ownership and membership rules.
|
||||||
|
*
|
||||||
|
* @param businessDate server-derived business date to evaluate inclusively
|
||||||
|
* @return deterministic options ordered by display name, student code, then account ID
|
||||||
|
* @throws IllegalArgumentException when {@code businessDate} is {@code null}
|
||||||
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<EligibleInternOption> eligibleInternOptions(LocalDate businessDate) {
|
||||||
|
if (businessDate == null) {
|
||||||
|
throw new IllegalArgumentException("Business date is required");
|
||||||
|
}
|
||||||
|
return internProfiles.findEligibleInternOptions(
|
||||||
|
GlobalRole.INTERN, AccountStatus.ACTIVE, InternshipStatus.ACTIVE, businessDate);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the cross-feature identity of a currently eligible Intern.
|
* Resolves the cross-feature identity of a currently eligible Intern.
|
||||||
*
|
*
|
||||||
|
|||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
package com.lab.labtimesheet.feature.account.service;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.lab.labtimesheet.config.TestcontainersConfiguration;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.GlobalRole;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.dto.EligibleInternOption;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.entity.AppUser;
|
||||||
|
import com.lab.labtimesheet.feature.account.model.entity.InternProfile;
|
||||||
|
import com.lab.labtimesheet.feature.account.repository.AppUserRepository;
|
||||||
|
import com.lab.labtimesheet.feature.account.repository.InternProfileRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
@Import(TestcontainersConfiguration.class)
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||||
|
class EligibleInternOptionIntegrationTest {
|
||||||
|
private static final Instant NOW = Instant.parse("2026-08-14T00:00:00Z");
|
||||||
|
private static final LocalDate BUSINESS_DATE = LocalDate.of(2026, 8, 14);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AccountService accounts;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AppUserRepository users;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InternProfileRepository internProfiles;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
private AppUser admin;
|
||||||
|
private int userSequence;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
admin = users.saveAndFlush(AppUser.bootstrapAdmin(
|
||||||
|
"picker-admin@example.com", "Picker Admin", "encoded-password", NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listsOnlyActiveInternsWithActiveInclusiveInternshipsInPickerOrder() {
|
||||||
|
long lowerBoundary = activeIntern("Alpha", "STU-100", BUSINESS_DATE, BUSINESS_DATE.plusDays(10));
|
||||||
|
long upperBoundary = activeIntern("Alpha", "STU-200", BUSINESS_DATE.minusDays(10), BUSINESS_DATE);
|
||||||
|
long laterName = activeIntern("Zeta", "STU-300", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
||||||
|
|
||||||
|
pendingInternWithActiveProfile("Ignored Pending", "STU-400");
|
||||||
|
long locked = activeIntern("Ignored Locked", "STU-500", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
||||||
|
lock(locked);
|
||||||
|
long deactivated = activeIntern(
|
||||||
|
"Ignored Deactivated", "STU-600", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
||||||
|
deactivate(deactivated);
|
||||||
|
activeMentorWithActiveProfile("Ignored Mentor", "STU-700");
|
||||||
|
activeInternWithNotStartedProfile("Ignored Not Started", "STU-800");
|
||||||
|
activeIntern("Ignored Ended", "STU-900", BUSINESS_DATE.minusDays(10), BUSINESS_DATE.minusDays(1));
|
||||||
|
long completed = activeIntern(
|
||||||
|
"Ignored Completed", "STU-1000", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
||||||
|
completeInternship(completed);
|
||||||
|
|
||||||
|
List<EligibleInternOption> options = accounts.eligibleInternOptions(BUSINESS_DATE);
|
||||||
|
|
||||||
|
assertThat(options).containsExactly(
|
||||||
|
new EligibleInternOption(
|
||||||
|
lowerBoundary, "Alpha", "STU-100", BUSINESS_DATE, BUSINESS_DATE.plusDays(10)),
|
||||||
|
new EligibleInternOption(
|
||||||
|
upperBoundary, "Alpha", "STU-200", BUSINESS_DATE.minusDays(10), BUSINESS_DATE),
|
||||||
|
new EligibleInternOption(
|
||||||
|
laterName, "Zeta", "STU-300", BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1)));
|
||||||
|
assertThat(options).extracting(EligibleInternOption::userId).doesNotHaveDuplicates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsMissingBusinessDate() {
|
||||||
|
assertThatThrownBy(() -> accounts.eligibleInternOptions(null))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Business date is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
private long activeIntern(String displayName, String studentCode, LocalDate startDate, LocalDate endDate) {
|
||||||
|
long userId = activeUser(GlobalRole.INTERN, displayName);
|
||||||
|
activeProfile(userId, studentCode, startDate, endDate);
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pendingInternWithActiveProfile(String displayName, String studentCode) {
|
||||||
|
long userId = pendingUser(GlobalRole.INTERN, displayName);
|
||||||
|
activeProfile(userId, studentCode, BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void activeMentorWithActiveProfile(String displayName, String studentCode) {
|
||||||
|
long userId = activeUser(GlobalRole.MENTOR, displayName);
|
||||||
|
activeProfile(userId, studentCode, BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void activeInternWithNotStartedProfile(String displayName, String studentCode) {
|
||||||
|
long userId = activeUser(GlobalRole.INTERN, displayName);
|
||||||
|
internProfiles.saveAndFlush(InternProfile.notStarted(
|
||||||
|
userId, studentCode, BUSINESS_DATE.minusDays(1), BUSINESS_DATE.plusDays(1), NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long activeUser(GlobalRole role, String displayName) {
|
||||||
|
AppUser user = AppUser.pending(nextEmail(), displayName, role, admin, NOW);
|
||||||
|
user.activate("encoded-password", NOW);
|
||||||
|
return users.saveAndFlush(user).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long pendingUser(GlobalRole role, String displayName) {
|
||||||
|
return users.saveAndFlush(AppUser.pending(nextEmail(), displayName, role, admin, NOW)).getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void activeProfile(long userId, String studentCode, LocalDate startDate, LocalDate endDate) {
|
||||||
|
InternProfile profile = InternProfile.notStarted(userId, studentCode, startDate, endDate, NOW);
|
||||||
|
profile.activate(NOW);
|
||||||
|
internProfiles.saveAndFlush(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void lock(long userId) {
|
||||||
|
assertThat(jdbc.update(
|
||||||
|
"""
|
||||||
|
update app_users
|
||||||
|
set account_status = 'LOCKED', locked_at = ?, updated_at = ?
|
||||||
|
where id = ?
|
||||||
|
""",
|
||||||
|
Timestamp.from(NOW), Timestamp.from(NOW), userId)).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deactivate(long userId) {
|
||||||
|
assertThat(jdbc.update(
|
||||||
|
"""
|
||||||
|
update app_users
|
||||||
|
set account_status = 'DEACTIVATED', deactivated_at = ?, updated_at = ?
|
||||||
|
where id = ?
|
||||||
|
""",
|
||||||
|
Timestamp.from(NOW), Timestamp.from(NOW), userId)).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeInternship(long userId) {
|
||||||
|
assertThat(jdbc.update(
|
||||||
|
"""
|
||||||
|
update intern_profiles
|
||||||
|
set internship_status = 'COMPLETED', completed_at = ?, updated_at = ?
|
||||||
|
where user_id = ?
|
||||||
|
""",
|
||||||
|
Timestamp.from(NOW), Timestamp.from(NOW), userId)).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nextEmail() {
|
||||||
|
return "picker-" + ++userSequence + "@example.com";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user