Skip to content

Add Projects feature and comprehensive test coverage#1

Merged
Jeremy8776 merged 7 commits into
masterfrom
fix/skills-persist-across-modes
May 17, 2026
Merged

Add Projects feature and comprehensive test coverage#1
Jeremy8776 merged 7 commits into
masterfrom
fix/skills-persist-across-modes

Conversation

@James-Chapman
Copy link
Copy Markdown
Collaborator

Summary

  • Add Projects tab with full CRUD for per-project context scoping (memory, rules, handoffs get their own data/projects/<slug>/ directories)
  • Fix skills vanishing when a mode is applied — backfill missing skills in states GET/POST and client-side loadFromServer/applyServerStates
  • Add Browse button for repo path in handoff modal and local folder linking in skills install modal
  • Improve .fb button visibility with dull purple styling (--accent-bg/--accent-line/--accent-hi) instead of transparent
  • Add 6 new smoke tests covering previously untested server modules: crypto.js, security.js, validation.js, backup.js, ranking.js, per-key-mutex.js
  • Extend 3 existing tests with edge cases: chunker (empty/corrupt/CRLF/oversized), vectorstore (replaceVectors, stale lifecycle, cosine boundaries), handoffs (updateHandoff, getHandoff, path traversal, slug collisions)

Test plan

  • npm run test:crypto — API key encrypt/decrypt, env override, corrupt file recovery
  • npm run test:security — request validation (loopback/CSRF), write-path protection (system dirs, .ssh/.aws, UNC)
  • npm run test:validation — memory/rules/states validators (all error paths)
  • npm run test:backup — read/write, backup/restore cycle, session log cap
  • npm run test:ranking — tokenize, scoreChunk, scoreSkills, chooseKeeper
  • npm run test:mutex — serial per key, parallel across keys, error isolation
  • npm run test:projects — 78 tests: library CRUD + slug edge cases + HTTP integration
  • All 15 existing smoke tests still pass
  • npm run check (lint:css, typecheck, lint, format:check) clean

…ules

- Add Projects tab with CRUD (server/lib/projects.js, router routes,
  UI tab/modal, store.js helpers) for per-project context scoping
- Fix skills vanishing on mode apply: backfill missing skills in states
  GET/POST, SS.loadFromServer(), SS.applyServerStates()
- Add Browse button for repo path in handoff modal and local folder
  linking in skills install modal
- Change .fb button styling from transparent to dull purple for visibility
- Add 6 new smoke tests (crypto, security, validation, backup, ranking,
  mutex) covering previously untested server modules
- Extend 3 existing tests (chunker, vectorstore, handoffs) with edge
  cases: empty/corrupt/oversized inputs, replaceVectors, stale index
  lifecycle, cosine similarity boundaries, updateHandoff, getHandoff,
  path traversal, slug collisions
@James-Chapman James-Chapman requested a review from Jeremy8776 May 17, 2026 14:00
@Jeremy8776
Copy link
Copy Markdown
Owner

Main blocker: ui/projects.js calls AppDialog.confirm() without await, so deleting a project proceeds immediately because the returned Promise is truthy. That means pressing Cancel will not stop deletion.

Also worth fixing before merge:

  • server/lib/projects.js removes the project from the registry before deleting the project directory, and swallows all delete errors. If the directory delete fails, the UI says it’s gone while files remain on disk.
  • ui/skills-ingest.js clears the local folder input when reopening the modal, but does not clear the stored _localPath, so the old path can still be submitted.

@James-Chapman
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15291e04cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/lib/handoffs.js Outdated

const taken = new Set([...readBodyDirSlugs(HANDOFFS_DIR), ...readBodyDirSlugs(ARCHIVE_DIR)]);
const slug = uniqueSlug(tag || title, taken);
const slug = uniqueSlug(title, taken);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep migration slugs stable when creating handoffs

This now derives every new handoff slug from title, which breaks callers that treat thread_tag as the stable identity key. In particular, migrateLegacyHandoff checks getHandoff(entry.slug) before import; because created slugs no longer match that legacy slug, re-running migration on the same source re-imports entries and creates duplicates (..., ...-2, etc.) instead of skipping existing records.

Useful? React with 👍 / 👎.

Comment thread server/lib/projects.js Outdated
Comment on lines +78 to +80
if (!Array.isArray(reg.projects)) reg.projects = [];
reg.projects.push(project);
writeRegistry(reg);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize registry object before appending projects

If projects.json contains valid but non-object JSON (for example []), readRegistry() returns that value and this code writes reg.projects onto an Array; JSON.stringify(reg) then serializes back to [] and drops the new project entry. The API still returns success and creates project directories, so a malformed-but-parseable registry causes silent metadata loss and orphaned on-disk project data.

Useful? React with 👍 / 👎.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +5 to +7
branches: [master]
push:
branches: [master]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Run CI on the repository’s active branch

These branch filters only trigger on master, so if development happens on main (as in this repo), none of the lint/test jobs run for normal pushes or PRs. That leaves the new checks effectively disabled for day-to-day work and allows regressions to merge without CI coverage.

Useful? React with 👍 / 👎.

Comment thread server/lib/projects.js Outdated
if (!Array.isArray(reg.projects)) return { ok: false, error: 'No projects' };
const project = reg.projects.find((p) => p.slug === slug);
if (!project) return { ok: false, error: 'Project not found' };
if (patch?.name) project.name = String(patch.name).trim();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject blank names in project updates

The update path checks patch?.name before trimming, so a whitespace-only string passes the condition and then gets stored as "". This bypasses the non-empty-name rule enforced at creation time and produces nameless projects in the registry/UI, which makes listings and management ambiguous.

Useful? React with 👍 / 👎.

Comment thread server/lib/projects.js Outdated
if (!name) return { ok: false, error: 'name is required' };

const reg = readRegistry();
const taken = new Set((reg.projects || []).map((p) => p.slug));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle non-array project lists before slug generation

This assumes reg.projects is an array and calls .map() directly; if the registry is parseable JSON but has an invalid projects type (for example a string), project creation throws a runtime exception instead of returning a validation error. In the HTTP path this bubbles into a 500, so a single malformed registry file can break all project creation requests.

Useful? React with 👍 / 👎.

Comment thread server/router.js Outdated
return json(res, result, result.ok ? 200 : 400);
}
if (p.startsWith('/api/projects/') && req.method === 'PATCH') {
const slug = decodeURIComponent(p.slice('/api/projects/'.length));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Guard malformed slug decoding in project routes

Decoding the path segment without a try/catch means malformed percent-encoding (for example /api/projects/%E0%A4%A) throws URIError and returns a 500 from the top-level error handler. This is a client input error and should be handled as a 400-style validation failure instead of surfacing as an internal server error.

Useful? React with 👍 / 👎.

…stry, clear _localPath on modal reopen

- ui/projects.js: await AppDialog.confirm({message, confirmText, danger})
  so Cancel actually stops deletion (previously the un-awaited Promise
  was truthy so deletion always proceeded)
- server/lib/projects.js: delete project directory before removing from
  registry; surface directory removal failures as {ok:true, warning:...}
  instead of silently swallowing errors
- ui/skills-ingest.js: clear SkillsIngest._localPath when reopening the
  connect modal so stale paths can't be submitted
- scripts/projects-smoke.js: add 5 tests for delete ordering,
  dir-failure warning, and no-warning on success
@James-Chapman
Copy link
Copy Markdown
Collaborator Author

Addressed all three issues in 0efc382:

1. AppDialog.confirm() missing await — Fixed in ui/projects.js:97-102. Now uses await AppDialog.confirm({message, confirmText, danger}) matching the pattern used everywhere else in the UI. Previously the un-awaited Promise was truthy so deletion always proceeded regardless of Cancel.

2. Directory delete ordering and error surfacing — Fixed in server/lib/projects.js:84-101. The directory is now deleted before removing the project from the registry. If directory removal fails, the response includes {ok: true, warning: Directory removal failed: ...} instead of silently swallowing the error. The project is still removed from the registry (the files are stranded either way), but the caller now gets told about it. Added 5 new tests covering: registry removal on dir failure, warning on failure, no warning on success, and delete ordering verification.

3. Stale _localPath on modal reopen — Fixed in ui/skills-ingest.js:115. Added SkillsIngest._localPath = ''; alongside the existing localPath.value = '' in openConnectModal(), so the stored path is always cleared when the modal opens. This is a UI-only change (no server behavior), so no server-side test is applicable.

Copy link
Copy Markdown
Owner

Thanks for the follow-up fixes. Two of the three review issues look resolved now:

  • ui/projects.js now awaits AppDialog.confirm(), so Cancel should correctly stop deletion.
  • ui/skills-ingest.js now clears _localPath when reopening the modal, so the stale local-folder path issue is fixed.

One issue still remains before I’d merge:

server/lib/projects.js still removes the project from the registry even when directory deletion fails. It now returns a warning, but because the response is still ok: true, the UI removes the project and shows it as deleted while data/projects/<slug> may remain orphaned on disk.

I’d change that flow so directory removal failure returns { ok: false, error } and the registry entry is only removed after successful deletion. A missing directory can still be treated as success.

Checks I ran locally on the updated PR:

  • npm run test:projects passed, 83/83
  • npm run test:modes passed
  • npm run test:security passed

npm run check still could not complete in my temporary worktree because node_modules/.bin/tsc was not installed there.

- server/lib/projects.js: deleteProject now returns {ok:false,error} when
  directory removal fails; registry entry only removed on success; missing
  directory treated as success (not an error)
- server/lib/projects.js: readRegistry normalizes corrupt JSON (array,
  non-object) into a valid {version,projects} shape so createProject and
  listProjects never throw on malformed registries
- server/lib/projects.js: updateProject rejects whitespace-only names
  instead of storing empty strings
- server/lib/handoffs.js: restore tag-first slug generation
  (tag || title) so migration slugs stay stable and re-running
  migration doesn't create duplicates
- server/router.js: guard decodeURIComponent in project PATCH/DELETE
  routes — malformed percent encoding returns 400 instead of 500
- .github/workflows/ci.yml: add 'main' branch to triggers
- ui/projects.js: await AppDialog.confirm (already fixed)
- ui/skills-ingest.js: clear _localPath on modal reopen (already fixed)
- scripts/projects-smoke.js: tests for readRegistry normalization,
  whitespace-only name rejection, delete-on-failure semantics,
  decodeURIComponent guard, tag-based slug derivation
- scripts/handoffs-smoke.js: test that slug derives from thread_tag
  when present
@James-Chapman
Copy link
Copy Markdown
Collaborator Author

Addressed all 6 review comments in commit 5f61eeb:

P1 — Handoff slug (tag-based migration compatibility): Restored \uniqueSlug(tag || title, taken)\ in server/lib/handoffs.js so migration slugs stay stable. Added test verifying \ hread_tag\ is used as slug seed when present.

P2 — Normalize registry before use:
eadRegistry()\ now normalizes corrupt JSON (arrays, non-objects) into a valid {version, projects:[]}\ shape. Added tests for array registry and create-after-normalize.

P1 — CI triggers: Added \main\ branch to both \pull_request\ and \push\ triggers in ci.yml.

P2 — Reject whitespace-only names in updateProject: \updateProject\ now trims then validates the name, returning {ok:false, error:\x27name cannot be empty\x27}\ for whitespace-only input. Added test.

P2 — Guard reg.projects before .map(): Handled by the readRegistry normalization —
eg.projects\ is now always an array. Removed redundant || []\ fallback and \if (!Array.isArray)\ guard.

P3 — Guard decodeURIComponent in project routes: PATCH and DELETE project routes now wrap \decodeURIComponent\ in try/catch, returning 400 for malformed percent encoding. Added HTTP tests.

@James-Chapman
Copy link
Copy Markdown
Collaborator Author

Re: \deleteProject\ — Fixed in 5f61eeb. The flow now works as you described:

  1. If the directory exists and removal fails → returns {ok:false, error}\ and the registry entry is not removed
  2. If the directory doesn't exist → treated as success (directory already gone)
  3. If directory removal succeeds → registry entry removed, returns {ok:true}\

Added tests for all three cases including verification that the project remains in the registry when deletion fails.

@Jeremy8776 Jeremy8776 merged commit eac213d into master May 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants