Skip to content
Closed
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
15 changes: 8 additions & 7 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1310,7 +1310,8 @@ function sanitizeDraft(draft) {
const sanitized = {};
EDITABLE_FIELDS.forEach((field) => {
// 🛡️ Sentinel: Enforce string coercion before trim() to prevent DoS via type confusion
sanitized[field] = String(draft?.[field] || '').trim().slice(0, 1000);
const value = draft?.[field];
sanitized[field] = String(value === 0 ? value : value || '').trim().slice(0, 1000);
});
// 🛡️ Sentinel: Strictly validate against allowed options to prevent injection
if (!sanitized.actualProgressStatus || !ACTUAL_PROGRESS_OPTIONS.includes(sanitized.actualProgressStatus)) {
Expand Down Expand Up @@ -1813,10 +1814,10 @@ const createNormalizedExternalRecord = (task, defaults = {}) => ({
actualStartDate: task.actualStartDate || '',
actualEndDate: task.actualEndDate || '',
predecessors: task.predecessors || defaults.predecessors || '',
budget: task.budget || defaults.budget || '',
actualCost: task.actualCost || defaults.actualCost || '',
budget: task.budget === 0 ? 0 : task.budget || defaults.budget || '',
actualCost: task.actualCost === 0 ? 0 : task.actualCost || defaults.actualCost || '',
sprint: task.sprint || defaults.sprint || '',
storyPoints: task.storyPoints || defaults.storyPoints || ''
storyPoints: task.storyPoints === 0 ? 0 : task.storyPoints || defaults.storyPoints || ''
Comment on lines +1817 to +1820

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Numeric zero fields appear empty

When imported fields contain numeric zeroes, renderEditorField displays them as blanks during editing. Users cannot distinguish preserved zeroes from missing values.

Prompt for agents
Numeric zero values introduced by createNormalizedExternalRecord remain numbers in state, but app.js renderEditorField initializes each input with value || '', so imported zero budget, actualCost, and storyPoints fields appear empty when edited. Initialize editor inputs without treating numeric zero as missing, while preserving the existing fallback for null, undefined, false, and empty strings as intended. Add regression coverage that opens each imported zero-valued task and verifies the corresponding editor input shows "0".
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

});

function getPhaseKey(task, index) {
Expand Down Expand Up @@ -1962,10 +1963,10 @@ function exportCsv() {
task.parentId || '',
task.depth,
task.predecessors || '',
task.budget || '',
task.actualCost || '',
task.budget === 0 ? 0 : task.budget || '',
task.actualCost === 0 ? 0 : task.actualCost || '',
task.sprint || '',
task.storyPoints || ''
task.storyPoints === 0 ? 0 : task.storyPoints || ''
];
});

Expand Down
33 changes: 33 additions & 0 deletions tests/e2e/scopeweave.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,39 @@ test.describe('ScopeWeave Planner', () => {
await expect(page).toHaveTitle('My New Project - ScopeWeave Planner');
});

test('preserves numeric zero values through seed normalization and persistence', async ({ page }) => {
await page.route('**/wbs.json', async (route) => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ __id: 'zero-budget', __depth: '3', phase: 'P5000', activity: 'Data', task: 'Zero budget', budget: 0, actualCost: 100, storyPoints: 1 },
{ __id: 'zero-cost', __depth: '3', phase: 'P5000', activity: 'Data', task: 'Zero cost', budget: 100, actualCost: 0, storyPoints: 1 },
{ __id: 'zero-points', __depth: '3', phase: 'P5000', activity: 'Data', task: 'Zero points', budget: 100, actualCost: 100, storyPoints: 0 },
{ __id: 'all-zero', __depth: '3', phase: 'P5000', activity: 'Data', task: 'All zero', budget: 0, actualCost: 0, storyPoints: 0 },
{ __id: 'missing-values', __depth: '3', phase: 'P5000', activity: 'Data', task: 'Missing values' }
])
}));
await page.evaluate(() => localStorage.clear());
await page.reload();

await expect(page.locator('tbody tr[data-task-id]')).toHaveCount(5);
await page.getByTestId('project-name-input').fill('Zero preservation round trip');
await expect.poll(() => page.evaluate(() => localStorage.getItem('scopeweave:planner-state:v1'))).not.toBeNull();
const savedTasks = await page.evaluate(() => JSON.parse(localStorage.getItem('scopeweave:planner-state:v1')).tasks);
const valuesById = Object.fromEntries(savedTasks.map((task) => [task.id, {
budget: task.budget,
actualCost: task.actualCost,
storyPoints: task.storyPoints
}]));
expect(valuesById).toEqual({
'zero-budget': { budget: 0, actualCost: 100, storyPoints: 1 },
'zero-cost': { budget: 100, actualCost: 0, storyPoints: 1 },
'zero-points': { budget: 100, actualCost: 100, storyPoints: 0 },
'all-zero': { budget: 0, actualCost: 0, storyPoints: 0 },
'missing-values': { budget: '', actualCost: '', storyPoints: '' }
});
});
Comment on lines +93 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Regression omits changed boundaries

The test stops after local persistence. It never opens the editor or exercises CSV export, so two modified zero-handling boundaries remain uncovered.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


[
{ name: 'desktop', width: 1440, height: 1000 },
{ name: 'mobile', width: 375, height: 667 }
Expand Down
Loading