fix(ci): repair the verification pipeline — fake-green checks, dead e2e suite, runtime coverage gaps - #563
Merged
Conversation
Three layers of this repo's verification reported success without checking anything, and the layer that could have caught the difference did not run. CI checks that checked nothing: - code-quality.yml's console.log / TODO / large-file checks all scanned `packages/`, a directory this repo has never had, each with `continue-on-error: true`. Replaced by scripts/check-source-hygiene.mjs, which scans src/test/e2e/scripts, fails on a hit, and fails loudly if a scanned directory disappears. - deploy-docs.yml copied QUICKSTART.md and PROJECT_SUMMARY.md unguarded; neither exists, so every triggering push failed the workflow. - `continue-on-error` removed from `pnpm lint` in both workflows — `objectstack lint` already exits 0 on warnings, so the flag hid only real errors. - .eslintrc.json removed (no eslint installed anywhere to read it) and CONTRIBUTING corrected to describe what `pnpm lint` actually does. - labeler.yml: three of seven rules pointed at paths that never existed and `ui` at a label the repo does not have. Globs repointed; a new test fails on a glob that matches nothing or a label that does not exist. - apps/docs (Next.js, 231 mdx pages, own lockfile) is now typechecked and built by docs-app.yml. Nothing compiled it before. The e2e suite could not run: - playwright.config.ts pointed baseURL at 4004 while the server serves 4001, declared no webServer, and no workflow ran it. - Both specs treated the data API's 401 as a pass, so even on the right port they proved only that a route was mounted. smoke.spec.ts also pinned a platform-owned health constant, and opportunity-lifecycle.spec.ts read the record from the wrong envelope key and omitted a required field. - The suite now boots the server, authenticates once via globalSetup, and asserts unconditionally — including that the data API IS gated. retries and trace are configured for CI; e2e.yml runs it. Typecheck blind spots: - tsconfig covered only src/, leaving test/, e2e/ and scripts/ unchecked. Widening `include` surfaced 45 real errors, all fixed here. - module mode moves to preserve/bundler (nothing here is emitted by tsc) and noEmit is pinned so no stray tsc run can scatter output into the dist/ that publish-marketplace.mjs uploads. - tsx is a real dependency behind `pnpm reconcile:analytics`; the command the analytics-reconcile script documented could not run. - wow1-live-schema.sh preflights the `ai` capability and explains why a local server does not mount it. Runtime coverage: - Hooks 4/24 -> 24/24 tested; flows 3/20 -> 17/20, including all six scheduled sweeps. - Hook-handler coverage: 23% -> 95% statements, 22% -> 95% functions, with vitest thresholds set just under the measured numbers. - Shared hook/flow harnesses replace three divergent copies of the in-memory data engine and add the query operators the old ones lacked; an equality-only engine silently matches nothing for the $lt/$nin filters every scheduled sweep uses. - test/runtime-coverage.test.ts fails when a hook or flow arrives untested. - docs-drift.test.ts no longer resolves flow files relative to cwd. Defect these tests surfaced: conditional edges nested inside a `loop` body never evaluate — the flow conversion pass wraps bare string conditions into CEL envelopes only for top-level edges, so a loop-nested condition falls through to the engine's legacy path and is string-compared. That leaves opportunity_stagnation, contract_renewal and campaign_enrollment inert past their gates. Pinned and documented in test/flow-scheduled.test.ts rather than fixed here: the fix changes what those production sweeps do and belongs in its own change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127MccuEdXF7pxJFmaiD7UU
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
CodeQL flagged it on #563. The file walks directories with readdirSync's withFileTypes and never stats anything — the import was left over from an earlier size-check draft that moved into scripts/check-source-hygiene.mjs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127MccuEdXF7pxJFmaiD7UU
…ipeline-issues-0c8u5k
main moved `days_in_stage` from a stored column to a formula over a new `stage_entry_date` (42a1d33, #489). PR checks run against the merge commit, so the branch went red on assertions that were correct when written: - opportunity_lifecycle now re-stamps `stage_entry_date` on a stage change instead of zeroing `days_in_stage`. The hook tests assert the stored column, and gain two cases the change makes load-bearing: the clock starts on insert (a deal that never moves must still be visible to the sweep), and an edit that does not change the stage must not reset it. - opportunity_stagnation filters `stage_entry_date < {TODAY() - 14}`, so the sweep tests seed dates rather than day counts. The boundary case now pins `$lt` (exactly-14 must not fire) and a null-clock row is added, since a null does not satisfy `$lt` and must be skipped. - The e2e lifecycle spec asserted `days_in_stage` off the PATCH response. Formulas are evaluated over query results and are not echoed on a write, so it re-reads the record and asserts the derived value there — which proves the formula resolves end to end rather than just that the hook wrote a date. Also anchors the repo-tree drift guard main added on REPO_ROOT. It read `join(docFile)` and `join('src', dir)` relative to cwd, the same defect this branch fixed in the rest of the file: correct from the repo root, ENOENT from anywhere else. Full gate green on the merge: 359 tests, 11 e2e specs against a cold database, hook coverage 94.9% statements / 95.3% functions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127MccuEdXF7pxJFmaiD7UU
yinlianghui
marked this pull request as ready for review
July 31, 2026 04:05
Merged
13 tasks
…ipeline-issues-0c8u5k
yinlianghui
pushed a commit
that referenced
this pull request
Jul 31, 2026
The push that merged main after #563 landed (efb0707) delivered a `synchronize` event — `Label Pull Request` and the Vercel check both ran on it — but none of the `pull_request`-triggered workflows started, so the head commit carried no CI result and the PR could not be evaluated. Empty commit to re-run them; no content change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127MccuEdXF7pxJFmaiD7UU
yinlianghui
pushed a commit
that referenced
this pull request
Jul 31, 2026
#565 landed the same CEL-envelope fix this branch carried, using the `P` tagged-template helper from @objectstack/spec — a cleaner notation, so the flow files take main's version wholesale. What remains here is the part #565 did not cover, plus the two red tests its merge left on main. main is currently RED. Both failures are tests this branch's parent (#563) introduced, and both fired exactly as designed: - flow-scheduled: the KNOWN DEFECT case asserted the stagnation gate stays shut. #565 opened it, so the test failed with the message it was written to emit — "gate now opens — rewrite this test". Rewritten here to assert the real behaviour (nudge created, idempotent across sweeps, re-arms once the task completes). - flow-record-change: `opportunity_approval` asserted `typeof startCondition === 'string'`. #565 converted it to an envelope. The assertion was pinning the NOTATION rather than the thing that matters, so it now accepts either form and checks a non-empty expression exists. Second defect #565 left reachable: `timestamp(currentContract.end_date)` throws "timestamp() requires a string in ISO 8601 format" because `end_date` is a DATE field arriving as YYYY-MM-DD. While the condition was a bare string it was never evaluated, so this was latent; wrapping it as a real envelope makes it throw mid-sweep. Appending T00:00:00Z fixes it — verified load-bearing by reverting it, which fails 4 contract_renewal tests. So on main as it stands, contract_renewal books nothing: the gate evaluates and then blows up. That is a live regression, not a theoretical one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127MccuEdXF7pxJFmaiD7UU
This was referenced Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Addresses every item in #495. Three layers of this repo's verification reported success without checking anything, and the layer that could have caught the difference never ran.
Everything below was verified by running it, not by reading it.
Type of Change
Related Issues
Fixes #495
Changes Made
CI checks that passed while checking nothing
code-quality.yml— the console.log, TODO/FIXME and large-file checks all scannedpackages/, a directory this repo has never had, each withcontinue-on-error: true. Replaced byscripts/check-source-hygiene.mjs, which scans the real tree (src,test,e2e,scripts), fails on a hit, and fails loudly if a scanned directory ever disappears — so it can never go vacuous again. The tree is clean on all three today, which is what makes the gate enforceable from day one.deploy-docs.yml— copiedQUICKSTART.mdandPROJECT_SUMMARY.mdwith no guard; neither has ever existed, so every triggering push failed the workflow. Removed, anddocs/is now copied without|| true.pnpm lint—continue-on-error: truedropped in both workflows.objectstack lintalready exits 0 on warnings and suggestions, so the flag suppressed nothing except the errors worth gating on..eslintrc.json— deleted. Noeslintpackage was installed anywhere to read it, and nothing referenced it.CONTRIBUTING.md:148told contributors to runpnpm lint"as linting"; it now explains thatobjectstack lintchecks metadata conventions, and points atpnpm typecheck/ the hygiene script for code-level checks..github/labeler.yml— of seven rules, three pointed at paths that have never existed (src/server.ts,src/engine/**,src/triggers/**,src/ui/**,tailwind.config.cjs,src/metadata/**) anduialso named a label the repo does not have, so it could not have applied even with a correct glob. Globs repointed at the real tree;test/labeler-config.test.tsnow fails on a glob that matches nothing or a label that does not exist. (Verified against the old config: it flags all eight dead globs plus the missing label.)apps/docs— a Next.js + Fumadocs site with 231 mdx pages, its ownpackage.jsonand its own lockfile, so rootpnpm install/typecheck/buildnever touched a file of it. Newdocs-app.ymlrunstypes:checkandbuildon changes. Both verified passing locally.The e2e suite could not run
playwright.config.tspointedbaseURLat 4004 whilepnpm dev/startand every doc serve 4001, declared nowebServer, and no workflow ran playwright — so the only tests exercising the real kernel had been dead for their whole life.[200, 401, 403], and atest.skip()), so even on the right port they could only ever prove a route was mounted. Accepting 200 there meant a regression exposing every account anonymously would have kept the suite green.smoke.spec.tsassertedbody.data.version === '1.0.0'— that is the ObjectStack health payload schema version, not hotcrm's; no change to this repo can affect it and any platform bump breaks it. Now asserts the shape; hotcrm's own version stays pinned against the manifest intest/smoke.test.ts, where it means something.opportunity-lifecycle.spec.tshad three independent reasons it could never have gone green on real behaviour: the self-skip, reading the record fromjson.datawhen the API answers{ object, id, record }, and omitting the requiredcrm_account(a 400, not a 401).webServerrunsbuild && start, waits on/api/v1/health), authenticates once ine2e/global-setup.ts, and asserts unconditionally — including a dedicated test that the data API is gated.retries: 2+trace: 'on-first-retry'in CI;e2e.ymlruns it.opportunity_lifecyclehook end to end (stage→probability derivation, close-won date stamping, thestage_entry_datestall clock and thedays_in_stageformula that reads off it, the closed-record freeze guard rejecting a business-field edit while accepting a narrative one).Typecheck blind spots
tsconfig.json'sincludewas["src/**/*", "objectstack.config.ts"], sotest/,e2e/andscripts/— including the 606-linescripts/analytics-reconcile/— were never typechecked. Widening it surfaced 45 real errors, all fixed here (the reconciler reached forobject/categoryField/measures/groupingsDownoff types that do not declare them; several tests had unnarrowedundefined).outDir: "./dist"was declared whiledist/holds theobjectstack buildartifactpublish-marketplace.mjsuploads — anytscrun without--noEmitwould have scattered.jsinto what ships.noEmitis now pinned in the config, so the footgun is gone regardless of how tsc is invoked. Module mode moves topreserve/bundler: nothing here is emitted by tsc, and it is what lets extensionless imports andimport.metaboth resolve.tsxis now a real devDependency behindpnpm reconcile:analytics. The script documentedpnpm tsx …as its run command, buttsxwas in nopackage.jsonand no script invoked it. The documented command now runs (exits 0, 22 surfaces reconciled/skipped).scripts/wow1-live-schema.shcalled/api/v1/ai/*, which nothing mounts locally —objectstack.config.tsdocuments thataiwas deliberately dropped fromrequires. It now preflights and explains that, instead of failing on a barecurl: 404twenty lines in.Runtime coverage
vitest.config.tsgains coverage config with thresholds set just under the measured numbers, so they lock in this work rather than aspire to it. Coverage is scoped tosrc/objects/*.hook.ts— the only executable code insrc/; including declarative metadata diluted the signal enough to hide the gap in the first place.test/runtime-coverage.test.tsenforces flow coverage structurally instead, failing when a registered hook or flow has no runtime test, with aPENDING_FLOWSlist that may only shrink (a stale entry fails too).test/helpers/hook-harness.ts,flow-harness.ts) replace three divergent copies of the in-memory data engine and add the query operators the old ones lacked. This mattered: an equality-only engine silently matches nothing for the$lt/$ninfilters every scheduled sweep uses, so those flows could not have been tested against it at all.test/docs-drift.test.tsno longer resolves source files via cwd-relative paths (including the repo-tree guard main added while this was in flight).Conditional edges nested inside a
loopbody never evaluate.AutomationEngine.registerFlowrunsapplyConversionsToFlow, which rewrites a bare stringconditioninto a{ dialect: 'cel', source }envelope — but only for a flow's top-leveledges. It does not recurse into the ADR-0031 control-flow regions (loop.config.body). A condition left as a bare string falls through to the engine's legacy template path, which substitutes{var}templates (there are none) and then string-compares the leftover expression text:Verified against
@objectstack/service-automation16.1.0: the same expression passed as an explicit CEL envelope evaluates correctly, which is why every top-level conditional edge in this app works and only the loop-nested ones do not.Three flows carry conditional edges inside a loop body and are therefore inert past that gate:
opportunity_stagnation,contract_renewal,campaign_enrollment. The stalled-deal sweep selects the right deals and then nudges nobody; the renewal sweep never books a renewal.This is pinned and documented in
test/flow-scheduled.test.ts(with a test demonstrating the available fix) rather than fixed here. Fixing it — authoring nested conditions as explicit CEL envelopes, or making the conversion pass recurse upstream — changes what these production sweeps do (they would begin creating tasks, opportunities and notifications), which deserves its own review rather than riding along in a verification-pipeline change. Happy to open that as a follow-up.Testing
pnpm test: 359 passed / 20 files (2 skipped, pre-existing)pnpm lintexits 0pnpm buildpnpm verify(now validate + typecheck + lint + hygiene + build + test) exits 0pnpm typecheckclean acrosssrc,test,e2e,scriptspnpm test:e2e— 11 passed against a freshly seeded databaseapps/docstypes:checkandbuildboth passChecklist
.changeset/verification-pipeline-repair.md)Additional Notes
package-lock.jsonwas regenerated for the two new devDependencies (tsx,@vitest/coverage-v8);check-stackblitz-lock.mjspasses.test:qawas removed. It ranobjectstack test, which scannedqa/*.test.json— a directory that does not exist — against port 3000, and exited 0. Verification pipeline: fake-green CI checks, dead e2e suite, runtime test coverage gaps #495 notesverifyomits it; a script that finds nothing and succeeds is the same fake-green pattern this PR is about, so it was deleted rather than wired in.PENDING_FLOWScarries three entries (campaign_enrollment,case_csat_followup,demo_bootstrap), each with a stated reason. The guard stops that list growing silently or going stale, but it is a real remaining gap rather than full coverage.task_recurrenceadvances a monthly series withDate.setMonth, so Jan 31 + 1 month lands on Mar 3 instead of clamping to Feb 28. The test asserts current behaviour and flags it for follow-up.