Skip to content

Close #1014, ALKiP notifications#1041

Draft
plocket wants to merge 17 commits intov5from
1014_breaking_news_2
Draft

Close #1014, ALKiP notifications#1041
plocket wants to merge 17 commits intov5from
1014_breaking_news_2

Conversation

@plocket
Copy link
Copy Markdown
Collaborator

@plocket plocket commented Feb 2, 2026

In this PR, I have:

  • Added tests for any new features or bug fixes (if relevant)
  • Added my changes to the CHANGELOG.md at the top, under the "Unreleased" section
  • Ensured issues that this PR closes will be automatically closed

TODO:

Reason for this PR

[Early PR for quick top-level sanity check review, failing tests are fine for now.]
Summary: Add notifications to ALKilnInThePlayground (ALKiP) for ALKiln changes
Details:
This is a testing framework. One of the environments in which it runs is on a test author's own server. ALKiP is a package of mine that helps authors runs these tests on their server. I work hard to ensure backwards compatibility, but it can be useful to keep both up to date with the other and sometimes to notify authors of other changes to their server that might affect how ALKiP is running. It's hard for authors to tell which versions match up with each other and when to update in general.

This ALKiln PR generates HTML that ALKiP can use to add notifications about relevant changes to ALKiln, to the author's server, or about whatever else ALKiln thinks is helpful. We will use notifications sparingly.

Demo of the visual appearance of a notification. Visual preview:

ALKiln_notification_preview

Links to any solved or related issues

Closes #1014

Any manual testing I have done to ensure my PR is working

Publishing to npm and testing on ALKiP

@plocket plocket marked this pull request as draft February 2, 2026 14:27
Copy link
Copy Markdown

@tobynorth tobynorth left a comment

Choose a reason for hiding this comment

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

Looks good to me! Didn't find any major issues. One high-level, almost certainly out of scope question for my own understanding: it seems like the need for this PR arose out of ALKiln and ALKiP being separate packages -- why is that? (would you ever want to install ALKiP without wanting to install ALKiln?)

And a general question: how are all the logging changes (artifacts.js, validate.js, Log.js) and path changes (files.js, session_vars.js, set_sources_path.js, scope.js) related to the new notifications feature? For clarity, I'd suggest either putting something about these changes in the PR description or moving them to separate PR(s).

Comment thread lib/setup/artifacts.js
Comment on lines 14 to +16
const argv = require(`minimist`)( process.argv.slice(2) );
const artifacts_path = files.make_artifacts_folder( argv.path );

const artifacts_path = files.make_artifacts_folder();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: argv is no longer used -- consider either cleaning it up or (if this was accidental) restoring it as an arg to make_artifacts_folder

Copy link
Copy Markdown
Collaborator Author

@plocket plocket Apr 7, 2026

Choose a reason for hiding this comment

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

Great point, thank you!

I realize you have no context, but I'm going to think about this "out loud", so if you do have thoughts I'd appreciate them. tl;dr: I think I could bring it back to add flexibility without any particular downside. [I think I should remove argv considering how session_vars finds the config path now, though I'm still not sure I'm a fan.]

If I recall correctly, I had meant to remove it. I think the caller is now meant to make sure the root of the command is where the artifacts folder should be. In ALKiP (a project that uses this lib), for example, the cwd argument for subprocess ensures the root is in /tmp. I'm actually not sure I like it as the behavior is a bit hidden. I remember having trouble wrangling the root of the path for ALKiP for some reason, though, and landing on this solution. Seems to me like I could have both without a problem. [Going to keep using cwd for now, though, since session_vars is strictly using cwd with no other arguments. It makes sense I suppose. We'd otherwise have to manage an additional env var. See session_vars.get_runtime_config_path().]

[Note to self: Worth adding some of these notes to the docs.]

Comment thread lib/steps.js
Comment on lines +124 to +126
// Save Scenario data in Scenario folder
// '@alkiln @generated'
// console.log( JSON.stringify( scenario ) );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question/nit: did you mean to commit these comments?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch!

Suggested change
// Save Scenario data in Scenario folder
// '@alkiln @generated'
// console.log( JSON.stringify( scenario ) );

Comment thread package.json
{
"name": "@suffolklitlab/alkiln",
"version": "5.15.4",
"version": "5.15.0-feat-news-17",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question (non-blocking): just to satisfy my own curiosity, what's going on with the version number here? (in particular, why did the patch version number drop from 4 to 0?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No problem: I just manage branch versions based off of their base branch. When I first diverged from the default branch, the version was 5.15.0, that's all. I'm curious to hear if you've seen other methods!

Comment thread lib/steps.js
scope.paths.scenario_report = `${ scope.paths.scenario }/report.txt`;
// If it's a generated Scenario, save the `.feature` file in here
// GENERATED_FILES_FOLDER_NAME
if ( scenario.gherkinDocument.uri.includes("_alkiln_generated") ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: _alkiln_generated is a magic string, ideally would be defined in a more maintainable location. (Where did it come from, anyway? I don't see any other references to it in this PR)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch here too. The name is already in globals.js and I should be using that instead. I made a variable for it and everything! Looks like I don't need a separate variable for that atm anyway.

Suggested change
if ( scenario.gherkinDocument.uri.includes("_alkiln_generated") ) {
if ( scenario.gherkinDocument.uri.includes(globals.generated_files_folder_name) ) {

Comment thread lib/breaking_news.js
Comment on lines +87 to +88
* Note that the function prints the value to the console before returning the
* value. This is the only way ALKilnInThePlayground can get the value.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: why is this? Seems cleaner to use a temp file; is that not feasible for some reason?

Comment thread lib/breaking_news.js
Comment on lines +360 to +373
for ( let subhead of shuffled_subheads ) {

if ( subhead.includes(`!`) && used_exclamation ) { continue; }
if ( subhead.includes(`?`) && used_question ) { continue; }
if ( subhead.includes(`"`) && used_quotes ) { continue; }
if ( subheads_used[ subhead ]) { continue; }

if ( subhead.includes(`!`)) { used_exclamation = true; }
if ( subhead.includes(`?`)) { used_question = true; }
if ( subhead.includes(`"`)) { used_quotes = true; }

final_subhead = subhead;
subheads_used[ subhead ] = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: it feels like it would be a little cleaner to refactor this into get_shuffled_subheads by just ensuring that a max of 1 subhead with each checked char (and no duplicate subheads) are added to the return array. Then in this function, you could just pop or shift an element off the array to pass to get_one_articles_parts

Comment thread lib/breaking_news.js
Comment on lines +480 to +484
let html_paragraphs = [];
for ( let one_paragraph of paragraphs ) {
html_paragraphs.push(`<p class="body_copy">${ one_paragraph }</p>`);
}
return html_paragraphs;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: this could be simplified to:

Suggested change
let html_paragraphs = [];
for ( let one_paragraph of paragraphs ) {
html_paragraphs.push(`<p class="body_copy">${ one_paragraph }</p>`);
}
return html_paragraphs;
return paragraphs.map(paragraph => `<p class="body_copy">${ paragraph }</p>`);

Comment thread lib/breaking_news.js
Comment on lines +625 to +641
if ( typeof list_or_str === `string` ) {
indent_str = ` `.repeat( indent );
return [ indent_str + list_or_str ];
}

let flattened = [];
for ( let nested_str_or_list of list_or_str ) {
let result = indent_and_flatten_html_list(
nested_str_or_list,
indent + 1
);
// // .concat() feels evil because it accepts a string as well
// flattened = flattened.concat( result );
flattened.push( ...result );
}

return flattened;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: I think this could be simplified to:

Suggested change
if ( typeof list_or_str === `string` ) {
indent_str = ` `.repeat( indent );
return [ indent_str + list_or_str ];
}
let flattened = [];
for ( let nested_str_or_list of list_or_str ) {
let result = indent_and_flatten_html_list(
nested_str_or_list,
indent + 1
);
// // .concat() feels evil because it accepts a string as well
// flattened = flattened.concat( result );
flattened.push( ...result );
}
return flattened;
return list_or_str.flatMap(nested_str_or_list =>
Array.isArray(nested_str_or_list)
? indent_and_flatten_html_list(nested_str_or_list, indent + 1)
: ` `.repeat(indent) + nested_str_or_list
);

Comment thread lib/breaking_news.js
}


function get_semver_parts( version_str ) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Consider using a 3rd party library to replace all your semver-related functions if you haven't already (example). Since this is neither related to the core ALKiln logic nor (it seems) something ALKiln needs to go outside the semver spec for, probably better to let someone else maintain it

Comment thread lib/breaking_news.js
// If we switch to module (import) instead of common (require), use
// https://nodejs.org/api/esm.html#importmetafilename for file path
let most_recent_file_sha = execSync(
`git rev-list -1 HEAD -- "${ __filename }"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: this is running on users' servers, right? Is it safe to assume they have git installed and have cloned the ALKiln repo locally?...

Comment thread lib/setup/artifacts.js
Comment on lines +20 to +26
const log = new Log({ path: artifacts_path, context: `artifacts` });
log.info({ code: `ALK0284`, context: `artifacts`,},
`Created the artifacts folder at "${ artifacts_path }"`
);

// This is just for artifacts, not any other config values. Leave the config
// Project name value as it is. Local developers find it useful to re-use it.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

What does "This" mean, Past Self? I think it meant that saving the artifacts path name here is unique in some way. Maybe because it's updated with every run of the tests. If so, we should write documentation about which config values get changed and when they get changed. Suggestion:

Changing config values:

  • Artifacts folder: name is different with each test run since artifacts are unique to each test run.
  • Project name: the env vars determine the Project name. Local devs often re-use the same Project. Other environments, like GitHub or ALKiP are one-use and manage their own Project name value.

Remove comment from code here:

Suggested change
const log = new Log({ path: artifacts_path, context: `artifacts` });
log.info({ code: `ALK0284`, context: `artifacts`,},
`Created the artifacts folder at "${ artifacts_path }"`
);
// This is just for artifacts, not any other config values. Leave the config
// Project name value as it is. Local developers find it useful to re-use it.
const log = new Log({ path: artifacts_path, context: `artifacts` });
log.info({ code: `ALK0284`, context: `artifacts`,},
`Created the artifacts folder at "${ artifacts_path }"`
);

Comment thread lib/steps.js
// Store path name for this Scenario's individualized report
scope.paths.scenario_report = `${ scope.paths.scenario }/report.txt`;
// If it's a generated Scenario, save the `.feature` file in here
// GENERATED_FILES_FOLDER_NAME
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
// GENERATED_FILES_FOLDER_NAME

Comment thread lib/steps.js
Comment on lines +29 to +30

const GENERATED_FILES_FOLDER_NAME = globals.generated_files_folder_name;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
const GENERATED_FILES_FOLDER_NAME = globals.generated_files_folder_name;

@plocket
Copy link
Copy Markdown
Collaborator Author

plocket commented Apr 7, 2026

Thanks for the excellent and detailed review! I've finally started in on this, though it'll stay here a while longer (see below).

And a general question: how are all the logging changes (artifacts.js, validate.js, Log.js) and path changes (files.js, session_vars.js, set_sources_path.js, scope.js) related to the new notifications feature? For clarity, I'd suggest either putting something about these changes in the PR description or moving them to separate PR(s).

Good insticts! The logging and path changes were indeed meant to be in a separate PR and I'll move them there and deal with those first before addressing the notification feature, so this PR is going to end up sitting on the back burner a little while longer unfortunately. And will probably run into merge conflicts unless I make a whole new PR, which I won't because I don't want to lose this awesome review. Some days you can't have it both ways ¯\_(ツ)_/¯

One high-level, almost certainly out of scope question for my own understanding: it seems like the need for this PR arose out of ALKiln and ALKiP being separate packages -- why is that? (would you ever want to install ALKiP without wanting to install ALKiln?)

I think it's understandable without too much further context. There are 4 environments in which ALKiln can run and ALKiP is just one of those. As such, though you'd never want to install ALKiP without ALKiln, you may very well want to install ALKiln without ALKiP. At least, we have some users who do that.

@plocket plocket changed the title Close #1014, ALKiP notifications Close #1049, ALKiP notifications Apr 19, 2026
@plocket plocket changed the title Close #1049, ALKiP notifications Close #1014, ALKiP notifications Apr 19, 2026
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.

Allow ALKiln to give notifications to ALKiP to show to authors in the Playground pre-run

2 participants