Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions _contprompts/millos_place_led_beautification_20261010.md

Large diffs are not rendered by default.

68 changes: 65 additions & 3 deletions src/components/ui-new/widgets/WorkplaceCompanion.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { WorkplaceCompanion } from './WorkplaceCompanion';
import { createWorkplace } from '../../../simulation/bilateralWorkplace';
import { useWorkplaceStore } from '../../../stores/workplaceStore';
import { useOperationsCampaignStore } from '../../../stores/operationsCampaignStore';
import { campaignRecord } from '../../../simulation/workplaceCampaign';
import { CAMERA_PRESETS, useCameraStore } from '../../CameraController';

beforeEach(() => useWorkplaceStore.setState({ workplace: createWorkplace() }));
beforeEach(() => {
useOperationsCampaignStore.setState({ orders: [], activeOrderId: null });
useWorkplaceStore.setState({
workplace: createWorkplace(),
participationMode: 'solo',
turn: null,
});
});
afterEach(cleanup);
const show = () => {
const onOpen = vi.fn();
Expand Down Expand Up @@ -64,7 +72,7 @@ describe('live working agreement companion', () => {
expect(
screen.getByText('Current terms are ready for your explicit approval.')
).toBeInTheDocument();
store.selectPlan('cover');
act(() => store.selectPlan('cover'));
rerender(<WorkplaceCompanion onOpen={vi.fn()} />);
expect(
screen.queryByText('Current terms are ready for your explicit approval.')
Expand Down Expand Up @@ -151,3 +159,57 @@ describe('live working agreement companion', () => {
expect(screen.getByText('200 kg.')).toBeInTheDocument();
});
});

it('shows linked handoff customer credit against its real target with blocked evidence', () => {
const store = useWorkplaceStore.getState();
store.beginImprovement('cooperative', store.workplace.revision, undefined, 'game');
store.proposeImprovement('briefing', 'packing');
const state = useWorkplaceStore.getState().workplace;
useWorkplaceStore.setState({
workplace: {
...state,
shippedKg: 20,
improvement: {
...state.improvement!,
mission: {
orderId: 'linked',
customer: 'Local bakery',
materialSessionId: 'session',
startingShippedKg: 0,
originalTargetKg: 1000,
targetKg: 800,
observedShippedKg: 200,
creditedKg: 200,
manifestIds: ['manifest'],
evidence: 'stale',
},
},
},
});
show();
expect(screen.getByText('200 / 800 kg; evidence blocked.')).toBeInTheDocument();
expect(screen.getByText('800 kg.')).toBeInTheDocument();
expect(screen.getByText('20 kg.')).toBeInTheDocument();
expect(
screen.getByText(/Packing proposes a paid handoff briefing, revision/)
).toBeInTheDocument();
});

it('retains handoff compensation and delivered recovery while current debt remains visible', () => {
const store = useWorkplaceStore.getState();
store.beginImprovement('cooperative', store.workplace.revision, undefined, 'game');
const state = structuredClone(useWorkplaceStore.getState().workplace);
state.improvement!.origin.members[0].compensationPaid = 8;
state.improvement!.origin.members[0].recoveryMinutes = 10;
state.finance.compensationPaid = 2;
state.members[0].compensationPaid = 2;
state.members[0].recoveryMinutes = 3;
state.members[0].recoveryOwedMinutes = 4;
useWorkplaceStore.setState({ workplace: state });
show();
expect(screen.getByText('10.00 illustrative credits.')).toBeInTheDocument();
expect(screen.getByText('13.0 min delivered; 4.0 min owed.')).toBeInTheDocument();
expect(screen.getByText('Retained compensation paid:')).toBeInTheDocument();
expect(screen.getByText('Teaching output:')).toBeInTheDocument();
expect(useWorkplaceStore.getState().workplace).toBe(state);
});
50 changes: 36 additions & 14 deletions src/components/ui-new/widgets/WorkplaceCompanion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ export function WorkplaceCompanion({ onOpen }: { onOpen: () => void }) {
const negotiation = thursdayNegotiation(state);
const improvement = improvementSummary(state);
const plan = WORKPLACE_PLANS.find((item) => item.id === state.planId);
const paid = guide.totals.members.reduce((sum, member) => sum + member.compensationPaid, 0);
const recovered = guide.totals.members.reduce((sum, member) => sum + member.recoveryMinutes, 0);
const owed = guide.totals.members.reduce((sum, member) => sum + member.recoveryOwedMinutes, 0);
const paid =
improvement?.lifetimeCompensation ??
guide.totals.members.reduce((sum, member) => sum + member.compensationPaid, 0);
const recovered =
improvement?.lifetimeRecoveryMinutes ??
guide.totals.members.reduce((sum, member) => sum + member.recoveryMinutes, 0);
const owed =
improvement?.currentRecoveryOwedMinutes ??
guide.totals.members.reduce((sum, member) => sum + member.recoveryOwedMinutes, 0);
const mission = workplaceMission(state);
const cover = state.members.find((member) => member.id === state.activeCoverMemberId);
const delivery = state.campaign
? guide.outcomes.delivery
Expand All @@ -25,6 +32,12 @@ export function WorkplaceCompanion({ onOpen }: { onOpen: () => void }) {
remainingKg: Math.max(0, state.targetKg - state.shippedKg),
status: 'pending',
};
const deliveredKg = mission?.creditedKg ?? delivery.deliveredKg;
const targetKg = mission?.targetKg ?? delivery.targetKg;
const remainingKg = mission
? Math.max(0, mission.originalTargetKg - mission.creditedKg)
: delivery.remainingKg;
const evidenceBlocked = mission ? mission.evidence !== 'current' : delivery.status === 'blocked';
const title = improvement
? `Handoff shift ${improvement.cycle}`
: guide.needsRemedy
Expand All @@ -38,7 +51,7 @@ export function WorkplaceCompanion({ onOpen }: { onOpen: () => void }) {
>
<h2 className="font-semibold text-white">Working agreement</h2>
<p className="mt-1 text-xs text-cyan-200">
{plan?.title ?? state.planId}, revision {state.revision}
{improvement?.arrangement ?? plan?.title ?? state.planId}, revision {state.revision}
</p>
{state.improvement?.advice && (
<p className="mt-2 text-xs text-slate-300">
Expand Down Expand Up @@ -84,27 +97,36 @@ export function WorkplaceCompanion({ onOpen }: { onOpen: () => void }) {
)}
<dl className="mt-3 space-y-1 border-t border-slate-700 pt-3 text-xs tabular-nums">
<div>
<dt className="inline text-slate-300">Dispatch receipts: </dt>
<dt className="inline text-slate-300">
{mission ? 'Customer dispatch receipts: ' : 'Teaching output: '}
</dt>
<dd className="inline">
{(improvement?.actualKg ?? delivery.deliveredKg).toFixed(0)} /{' '}
{delivery.targetKg.toFixed(0)} kg
{delivery.status === 'blocked' && '; evidence blocked'}.
{deliveredKg.toFixed(0)} / {targetKg.toFixed(0)} kg
{evidenceBlocked && '; evidence blocked'}.
</dd>
</div>
{workplaceMission(state) && (
{improvement && mission && (
<div>
<dt className="inline text-slate-300">This handoff shift’s dispatch: </dt>
<dd className="inline">{improvement.actualKg.toFixed(0)} kg.</dd>
</div>
)}
{mission && (
<div>
<dt className="inline text-slate-300">Full customer commitment remaining: </dt>
<dd className="inline">
{(improvement?.remainingKg ?? delivery.remainingKg).toFixed(0)} kg.
</dd>
<dd className="inline">{remainingKg.toFixed(0)} kg.</dd>
</div>
)}
<div>
<dt className="inline text-slate-300">Compensation paid: </dt>
<dt className="inline text-slate-300">
{improvement ? 'Retained compensation paid: ' : 'Compensation paid: '}
</dt>
<dd className="inline">{paid.toFixed(2)} illustrative credits.</dd>
</div>
<div>
<dt className="inline text-slate-300">Recovery: </dt>
<dt className="inline text-slate-300">
{improvement ? 'Retained recovery: ' : 'Recovery: '}
</dt>
<dd className={`inline ${owed > 0 ? 'text-amber-100' : ''}`}>
{recovered.toFixed(1)} min delivered; {owed.toFixed(1)} min owed.
</dd>
Expand Down
197 changes: 186 additions & 11 deletions src/components/ui-new/widgets/WorkplaceImprovementPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { WorkplaceImprovementPanel } from './WorkplaceImprovementPanel';
import { WorkplaceParticipation } from './WorkplaceParticipation';
import { turnChecks, type RoleCommand } from '../../../simulation/workplaceParticipation';
import { useWorkplaceStore } from '../../../stores/workplaceStore';
import { createWorkplace } from '../../../simulation/bilateralWorkplace';
import { useOperationsCampaignStore } from '../../../stores/operationsCampaignStore';
Expand All @@ -16,23 +18,30 @@ import { captureCurrentPackingPlant } from '../../../stores/workplaceStore';
import { derivePackingAdvice } from '../../../simulation/workplaceAdvice';
const report = vi.fn();
beforeEach(() => {
useWorkplaceStore.setState({ workplace: createWorkplace() });
useWorkplaceStore.setState({
workplace: createWorkplace(),
participationMode: 'solo',
turn: null,
});
useOperationsCampaignStore.setState({ orders: [], activeOrderId: null });
report.mockReset();
useGameSimulationStore.getState().setGameSpeed(0);
});
afterEach(cleanup);
const click = (name: string) => fireEvent.click(screen.getByRole('button', { name, exact: true }));
const mount = () =>
const mount = (participation = false) =>
render(
<WorkplaceImprovementPanel
profile="cooperative"
activate={async () => {
useWorkplaceStore.getState().activate(useWorkplaceStore.getState().workplace.revision);
}}
busy={false}
report={report}
/>
<>
{participation && <WorkplaceParticipation report={report} />}
<WorkplaceImprovementPanel
profile="cooperative"
activate={async () => {
useWorkplaceStore.getState().activate(useWorkplaceStore.getState().workplace.revision);
}}
busy={false}
report={report}
/>
</>
);

describe('choice-first handoff interface', () => {
Expand Down Expand Up @@ -187,3 +196,169 @@ describe('choice-first handoff interface', () => {
act(() => store.stop());
});
});

describe('predict then rehearse together', () => {
it('freezes the optional prediction, refuses a false verdict on abstention and resets only the comparison', async () => {
const state = useWorkplaceStore.getState().workplace;
const stock = cloneMaterialFlowData(useMaterialFlowStore.getState());
const speed = useGameSimulationStore.getState().gameSpeed;
render(<MatchedPackingRehearsal state={state} report={report} />);
const prediction = screen.getByRole('combobox', {
name: 'Our packing prediction (optional)',
exact: true,
});
expect(prediction).toHaveValue('');
fireEvent.change(prediction, { target: { value: 'higher' } });
click('Run matched packing rehearsal');
await waitFor(() => expect(prediction).toBeDisabled());
const review = await screen.findByRole('region', { name: 'Packing prediction review' });
expect(
within(review).getByText(/Our prediction: Buffer range entirely above/)
).toBeInTheDocument();
expect(within(review).getByText(/insufficient matching evidence/)).toBeInTheDocument();
expect(within(review).queryByText(/support our prediction/)).not.toBeInTheDocument();
expect(
within(review).getByText(/More packed stock does not establish a truck departure/)
).toBeInTheDocument();
click('Prepare another prediction');
expect(
screen.queryByRole('region', { name: 'Packing prediction review' })
).not.toBeInTheDocument();
expect(prediction).toBeEnabled();
expect(prediction).toHaveValue('');
await waitFor(() => expect(prediction).toHaveFocus());
expect(useWorkplaceStore.getState().workplace).toBe(state);
expect(cloneMaterialFlowData(useMaterialFlowStore.getState())).toEqual(stock);
expect(useGameSimulationStore.getState().gameSpeed).toBe(speed);
expect(report).not.toHaveBeenCalled();
});

it('allows a skipped prediction and suppresses all learning conclusions when inputs change', async () => {
render(<MatchedPackingRehearsal state={createWorkplace()} report={report} />);
click('Run matched packing rehearsal');
const review = await screen.findByRole('region', { name: 'Packing prediction review' });
expect(within(review).getByText(/none recorded for this run/)).toBeInTheDocument();
act(() => useGameSimulationStore.getState().setGameSpeed(30));
expect(
screen.queryByRole('region', { name: 'Packing prediction review' })
).not.toBeInTheDocument();
expect(screen.getByRole('status')).toHaveTextContent('comparison is stale');
});
});

it('shows proposal and episode controls only to their current owner in separate turns', () => {
const store = useWorkplaceStore.getState();
store.setParticipationMode('separate-turns');
store.beginImprovement('cooperative', store.workplace.revision, undefined, 'workshop');
mount();
const packing = screen.getByRole('button', { name: 'Packing proposes a paid handoff briefing' });
const maintenance = screen.getByRole('button', {
name: 'Maintenance proposes a small packing buffer',
});
expect(packing).toBeDisabled();
expect(maintenance).toBeDisabled();
expect(screen.getByLabelText('Pressure episode')).toBeEnabled();
act(() => useWorkplaceStore.getState().startTurn('packing'));
expect(packing).toBeEnabled();
expect(maintenance).toBeDisabled();
expect(screen.getByLabelText('Pressure episode')).toBeDisabled();
click('Packing proposes a paid handoff briefing');
expect(useWorkplaceStore.getState().turn).toBeNull();
expect(screen.getByRole('button', { name: 'Hear the adviser response' })).toBeDisabled();
expect(screen.getByLabelText('Challenge author')).toBeDisabled();
expect(screen.getByLabelText('Challenge author')).toHaveValue('');
});

it('requires the adviser checks, then keeps understanding, policy choice and handover deliberate', async () => {
const get = () => useWorkplaceStore.getState();
get().setParticipationMode('separate-turns');
get().beginImprovement('cooperative', get().workplace.revision, undefined, 'workshop');
get().startTurn('maintenance');
get().runTurnCommand(get().turn!.token, {
type: 'proposeImprovement',
args: ['buffer', 'maintenance'],
});
get().startTurn('mind');
mount(true);
expect(screen.getByRole('button', { name: 'Hear the adviser response' })).toBeDisabled();
for (const q of turnChecks(get().workplace))
fireEvent.change(screen.getByLabelText(`Turn check: ${q.id}`), {
target: { value: q.correct },
});
expect(screen.getByRole('button', { name: 'Hear the adviser response' })).toBeEnabled();
click('Hear the adviser response');
expect(get().turn).toBeNull();
fireEvent.change(screen.getByLabelText('Hand over to fictional role'), {
target: { value: 'packing' },
});
click('Begin participant turn');
await waitFor(() => expect(screen.getByText(/Only Packing may/)).toHaveFocus());
const understand = screen.getByRole('button', { name: 'Confirm handoff understanding: Packing' });
const approve = screen.getByRole('button', { name: 'Approve handoff: Packing' });
expect(understand).toBeDisabled();
const finance = structuredClone(get().workplace.finance);
for (const q of turnChecks(get().workplace))
fireEvent.change(screen.getByLabelText(`Turn check: ${q.id}`), {
target: { value: q.correct },
});
expect(
get().workplace.members.every((member) => !member.understood && member.ballot === null)
).toBe(true);
expect(understand).toBeEnabled();
expect(approve).toBeDisabled();
click('Confirm handoff understanding: Packing');
expect(approve).toBeEnabled();
click('Decline handoff: Packing');
expect(get().workplace.members.find((member) => member.id === 'packing')?.ballot).toBe(false);
expect(
get()
.workplace.members.filter((member) => member.id !== 'packing')
.every((member) => member.ballot === null)
).toBe(true);
expect(get().workplace.finance).toEqual(finance);
click('End participant turn');
await waitFor(() => expect(screen.getByLabelText('Hand over to fictional role')).toHaveFocus());
});

it('keeps review ballots with each owner and final settlement with the facilitator', () => {
const get = () => useWorkplaceStore.getState();
get().setParticipationMode('separate-turns');
get().beginImprovement('cooperative', get().workplace.revision, undefined, 'workshop');
const command = (actor: string, action: RoleCommand) => {
get().startTurn(actor);
if (get().workplace.phase === 'deliberating')
for (const q of turnChecks(get().workplace))
get().answerTurnCheck(get().turn!.token, q.id, q.correct);
expect(get().runTurnCommand(get().turn!.token, action).changed).toBe(true);
get().endTurn();
};
command('maintenance', { type: 'proposeImprovement', args: ['buffer', 'maintenance'] });
command('mind', { type: 'acknowledgeImprovement', args: [] });
get().stop();
command('mind', { type: 'acknowledgeImprovementReview', args: [] });
get().startTurn('packing');
mount();
for (const member of get().workplace.members) {
const select = screen.getByLabelText(`${member.role} review decision`);
if (member.id !== 'packing') {
expect(select).toBeDisabled();
act(() => get().startTurn(member.id));
}
expect(select).toBeEnabled();
fireEvent.change(select, { target: { value: 'amend' } });
expect(get().turn).toBeNull();
expect(get().workplace.improvement?.ballots[member.id]).toBe('amend');
}
const settle = screen.getByRole('button', { name: 'Finalise handoff review' });
expect(settle).toBeEnabled();
act(() => get().startTurn('quality'));
expect(settle).toBeDisabled();
act(() => get().endTurn());
click('Finalise handoff review');
expect(get().workplace.improvement?.verdict).toBe('amend');
const next = screen.getByRole('button', { name: 'Open next handoff shift' });
expect(next).toBeEnabled();
act(() => get().startTurn('packing'));
expect(get().turn?.actorId).toBe('packing');
expect(next).toBeDisabled();
});
Loading
Loading